import { MAPS, BUILDINGS, TERRAIN, RESOURCES } from './data.js'; export const MAX_ACTIONS = 3; const SAVE_KEY = 'empire-web-save-v1'; const DEFAULT_MORALE = 80; const TERRAIN_BONUS = { plains: 1.0, forest: 1.2, mountain: 1.5, city: 1.3 }; const ACCEPT_RATES = { aggressive: [0.15, 0.20, 0.40], defensive: [0.50, 0.70, 0.50], mercantile: [0.40, 0.50, 0.80], diplomatic: [0.80, 0.85, 0.70], }; const TRADE_MARKUP = { aggressive: 1.5, defensive: 1.2, mercantile: 0.8, diplomatic: 1.0 }; const clone = (x) => JSON.parse(JSON.stringify(x)); const clamp0 = (n) => Math.max(0, n); export class Game { constructor() { this.state = null; this._handlers = new Set(); this._id = 0; } on(fn) { this._handlers.add(fn); return () => this._handlers.delete(fn); } emit(type = 'change', payload = null) { for (const fn of this._handlers) fn(this.state, type, payload); } commit(type = 'change', payload = null) { this.save(); this.emit(type, payload); } uid(prefix) { return `${prefix}_${Date.now().toString(36)}_${this._id++}`; } newGame({ mapId, factionId, identity }) { const map = MAPS[mapId] || MAPS.mainland; const territories = {}; for (const t of map.territories) { territories[t.id] = { ...clone(t), buildings: [] }; } const factions = {}; for (const f of map.factions) { const tpl = clone(f); factions[f.id] = { ...tpl, territories: tpl.territories.filter((id) => territories[id]), }; } for (const f of Object.values(factions)) { f.totalArmies = f.territories.reduce((s, id) => s + territories[id].armies, 0); } const faction = factions[factionId] || map.factions[0]; const ident = { leaderName: (identity?.leaderName || '').trim() || '君主', nationName: (identity?.nationName || '').trim() || faction.name, slogan: (identity?.slogan || '').trim() || '榮耀就在前方!', }; this.state = { version: 1, turn: 1, mapId: map.id, actionsLeft: MAX_ACTIONS, playerFactionId: faction.id, identity: ident, territories, factions, diplomacy: [], log: [], isOver: false, winner: null, stats: { battlesWon: 0, battlesLost: 0, recruited: 0, captured: 0 }, startedAt: Date.now(), }; this.addLog(`${ident.leaderName} 登基,成為 ${ident.nationName} 的君主。格言:「${ident.slogan}」`, 'story'); this.commit('new', { mapId: map.id, factionId: faction.id }); return this.state; } loadFromStorage() { try { const raw = localStorage.getItem(SAVE_KEY); if (!raw) return null; const data = JSON.parse(raw); if (!data || !data.territories || !data.factions) return null; for (const t of Object.values(data.territories)) { if (!Array.isArray(t.buildings)) t.buildings = []; if (!t.resources) t.resources = { gold: 0, food: 0, wood: 0, stone: 0 }; } for (const f of Object.values(data.factions)) { f.territories = (f.territories || []).filter((id) => data.territories[id]); f.totalArmies = f.territories.reduce((s, id) => s + data.territories[id].armies, 0); } if (!Array.isArray(data.log)) data.log = []; if (!Array.isArray(data.diplomacy)) data.diplomacy = []; if (!data.stats) data.stats = { battlesWon: 0, battlesLost: 0, recruited: 0, captured: 0 }; if (typeof data.actionsLeft !== 'number') data.actionsLeft = MAX_ACTIONS; this.state = data; this.emit('load'); return data; } catch { return null; } } hasSave() { try { return !!localStorage.getItem(SAVE_KEY); } catch { return false; } } save() { if (!this.state) return; try { localStorage.setItem(SAVE_KEY, JSON.stringify(this.state)); } catch {} } clearSave() { try { localStorage.removeItem(SAVE_KEY); } catch {} } get map() { return MAPS[this.state.mapId]; } get player() { return this.state.factions[this.state.playerFactionId]; } faction(id) { return this.state.factions[id] || null; } territory(id) { return this.state.territories[id] || null; } isPlayer(id) { return id === this.state.playerFactionId; } ownerName(id) { return id ? (this.faction(id)?.name || '未知') : '無主之地'; } ownedByPlayer(id) { return this.territory(id)?.owner === this.state.playerFactionId; } addLog(text, kind = 'info') { this.state.log.push({ turn: this.state.turn, text, kind }); if (this.state.log.length > 200) this.state.log = this.state.log.slice(-200); } syncArmies(fid) { const f = this.faction(fid); if (!f) return; f.totalArmies = this.factionTerritories(fid).reduce((sum, t) => sum + t.armies, 0); } playerTerritories() { return this.player.territories.map((id) => this.territory(id)).filter(Boolean); } factionTerritories(fid) { const f = this.faction(fid); return f ? f.territories.map((id) => this.territory(id)).filter(Boolean) : []; } income(fid = this.state.playerFactionId) { const out = { gold: 0, food: 0, wood: 0, stone: 0 }; for (const t of this.factionTerritories(fid)) { out.gold += t.resources.gold + (t.buildings.includes('market') ? 2 : 0); out.food += t.resources.food; out.wood += t.resources.wood; out.stone += t.resources.stone; } return out; } upkeep(fid = this.state.playerFactionId) { const units = this.factionTerritories(fid).reduce((s, t) => s + t.armies, 0); return { gold: 0, food: units, wood: 0, stone: 0 }; } recruitCost(tid) { const t = this.territory(tid); const gold = t?.buildings.includes('barracks') ? 2 : 3; return { gold, food: 2 }; } maxRecruit(tid) { const t = this.territory(tid); if (!t) return 0; const cost = this.recruitCost(tid); return Math.max(0, Math.min( Math.floor(this.player.gold / cost.gold), Math.floor(this.player.food / cost.food), )); } canAfford(cost) { return (this.player.gold >= (cost.gold || 0)) && (this.player.food >= (cost.food || 0)) && (this.player.wood >= (cost.wood || 0)) && (this.player.stone >= (cost.stone || 0)); } spend(cost) { for (const k of ['gold', 'food', 'wood', 'stone']) { if (cost[k]) this.player[k] = clamp0(this.player[k] - cost[k]); } } spendAction() { this.state.actionsLeft = Math.max(0, this.state.actionsLeft - 1); } get outOfActions() { return this.state.actionsLeft <= 0; } // ── Player orders ────────────────────────────────────────────────────────── recruit(tid, n) { const t = this.territory(tid); if (!t) return { ok: false, msg: '找不到該疆域。' }; if (t.owner !== this.state.playerFactionId) return { ok: false, msg: `你並不擁有 ${t.name}。` }; if (this.outOfActions) return { ok: false, msg: '本回合已無行動,請結束回合。' }; n = Math.floor(n); if (!n || n < 1) return { ok: false, msg: '至少徵募 1 名士兵。' }; const cost = this.recruitCost(tid); if (!this.canAfford({ gold: cost.gold * n, food: cost.food * n })) { return { ok: false, msg: `資源不足:需要 ${cost.gold * n} 金幣與 ${cost.food * n} 糧食。` }; } this.spend({ gold: cost.gold * n, food: cost.food * n }); t.armies += n; this.player.totalArmies += n; this.state.stats.recruited += n; this.spendAction(); this.addLog(`在 ${t.name} 徵募了 ${n} 名士兵。`, 'player'); this.commit('recruit', { tid, n }); return { ok: true, n, tid, cost, left: this.state.actionsLeft }; } move(fromId, toId, n) { const from = this.territory(fromId); const to = this.territory(toId); if (!from || !to) return { ok: false, msg: '找不到該疆域。' }; if (from.owner !== this.state.playerFactionId) return { ok: false, msg: `你並不擁有 ${from.name}。` }; if (to.owner !== this.state.playerFactionId) return { ok: false, msg: `你並未控制 ${to.name}。` }; if (!from.adjacentTo.includes(toId)) return { ok: false, msg: `${to.name} 與 ${from.name} 並不相鄰。` }; if (this.outOfActions) return { ok: false, msg: '本回合已無行動,請結束回合。' }; const movable = from.armies - 2; if (movable <= 0) return { ok: false, msg: `${from.name} 至少需留守 2 名士兵。` }; n = n == null ? movable : Math.floor(n); n = Math.min(n, movable); if (n < 1) return { ok: false, msg: '沒有可調動的士兵。' }; from.armies -= n; to.armies += n; this.spendAction(); this.addLog(`從 ${from.name} 調動 ${n} 名士兵前往 ${to.name}。`, 'player'); this.commit('move', { fromId, toId, n }); return { ok: true, n, fromId, toId, left: this.state.actionsLeft }; } resolveCombat(attackerUnits, defenderUnits, terrain, defenderTerritory) { const terrainBonus = TERRAIN_BONUS[terrain] ?? 1; const wallBonus = defenderTerritory?.buildings.includes('walls') ? 0.3 : 0; const totalDefBonus = terrainBonus + wallBonus; const attackPower = attackerUnits * (DEFAULT_MORALE / 100); const defensePower = defenderUnits * (DEFAULT_MORALE / 100) * totalDefBonus; const ratio = attackPower / (defensePower || 1); const log = []; log.push(`進攻戰力 ${attackPower.toFixed(1)},防守戰力 ${defensePower.toFixed(1)}。`); log.push(`${TERRAIN[terrain]?.label || terrain}地形提供 ×${terrainBonus} 防禦。`); if (wallBonus > 0) log.push(`城牆額外提供 +${wallBonus} 防禦。`); let outcome; let attackerCasualties; let defenderCasualties; let captured = false; if (defenderUnits <= 0) { outcome = 'uncontested'; attackerCasualties = 0; defenderCasualties = 0; captured = true; log.push('前方沒有守軍阻擋。'); } else if (ratio >= 2.0) { outcome = 'decisive_victory'; attackerCasualties = Math.floor(attackerUnits * 0.1); defenderCasualties = defenderUnits; captured = true; log.push('壓倒性勝利!守軍全面潰散。'); } else if (ratio >= 1.2) { outcome = 'victory'; attackerCasualties = Math.floor(attackerUnits * 0.25); defenderCasualties = Math.floor(defenderUnits * 0.6); captured = true; log.push('勝利!疆域已落入你手。'); } else if (ratio >= 0.8) { outcome = 'pyrrhic_victory'; attackerCasualties = Math.floor(attackerUnits * 0.5); defenderCasualties = Math.floor(defenderUnits * 0.4); captured = ratio >= 1.0; log.push(captured ? '慘勝——戰場屬於你,但代價沉重。' : '血戰僵持,進攻方被迫撤退。'); } else { outcome = 'defeat'; attackerCasualties = Math.floor(attackerUnits * 0.6); defenderCasualties = Math.floor(defenderUnits * 0.15); captured = false; log.push('戰敗!攻勢在守軍面前崩潰。'); } log.push(`我軍傷亡 ${attackerCasualties},敵軍傷亡 ${defenderCasualties}。`); return { outcome, attackerCasualties, defenderCasualties, captured, ratio, log }; } attack(fromId, toId, n) { const from = this.territory(fromId); const to = this.territory(toId); if (!from || !to) return { ok: false, msg: '找不到該疆域。' }; if (from.owner !== this.state.playerFactionId) return { ok: false, msg: `你並不擁有 ${from.name}。` }; if (to.owner === this.state.playerFactionId) return { ok: false, msg: `你已掌握 ${to.name}。` }; if (!from.adjacentTo.includes(toId)) return { ok: false, msg: `${to.name} 與 ${from.name} 並不相鄰。` }; if (this.outOfActions) return { ok: false, msg: '本回合已無行動,請結束回合。' }; if (from.armies < 1) return { ok: false, msg: `${from.name} 已無駐軍。` }; n = n == null ? from.armies : Math.floor(n); n = Math.min(Math.max(1, n), from.armies); if (n < 1) return { ok: false, msg: '至少派出 1 名士兵。' }; const defenderFactionId = to.owner; const defenderName = this.ownerName(defenderFactionId); const preDef = to.armies; const result = this.resolveCombat(n, preDef, to.type, to); const sent = n; const survivors = Math.max(0, sent - result.attackerCasualties); from.armies = Math.max(0, from.armies - sent); this.player.totalArmies = Math.max(0, this.player.totalArmies - result.attackerCasualties); let eliminated = null; if (result.captured) { if (defenderFactionId) { const old = this.faction(defenderFactionId); if (old) { to.armies = Math.max(0, to.armies - result.defenderCasualties); old.territories = old.territories.filter((id) => id !== toId); old.totalArmies = Math.max(0, old.totalArmies - result.defenderCasualties); if (old.territories.length === 0) { eliminated = old; this.addLog(`${old.name} 已從地圖上滅亡。`, 'critical'); } } } to.owner = this.state.playerFactionId; to.armies = survivors; this.player.territories.push(toId); this.state.stats.battlesWon += 1; this.state.stats.captured += 1; this.addLog(`你從 ${defenderName} 手中奪下 ${to.name},${survivors} 名士兵倖存。`, 'victory'); } else { to.armies = Math.max(0, to.armies - result.defenderCasualties); if (defenderFactionId) { const old = this.faction(defenderFactionId); if (old) old.totalArmies = Math.max(0, old.totalArmies - result.defenderCasualties); } this.state.stats.battlesLost += 1; this.addLog(`對 ${to.name} 的攻勢失敗,損失 ${result.attackerCasualties} 名士兵。`, 'defeat'); } this.spendAction(); this.syncArmies(this.state.playerFactionId); const payload = { fromId, toId, sent, defenderUnits: preDef, defenderName, result, survivors, eliminated, }; const win = this.checkWinner(); if (win) this.finish(win); this.commit('battle', payload); return { ok: true, ...payload, win, left: this.state.actionsLeft }; } build(tid, type) { const t = this.territory(tid); const def = BUILDINGS[type]; if (!t || !def) return { ok: false, msg: '未知的建築。' }; if (t.owner !== this.state.playerFactionId) return { ok: false, msg: `你並不擁有 ${t.name}。` }; if (this.outOfActions) return { ok: false, msg: '本回合已無行動,請結束回合。' }; if (t.buildings.includes(type)) return { ok: false, msg: `${t.name} 已建有${def.label}。` }; if (!this.canAfford(def.cost)) { const need = Object.entries(def.cost).map(([k, v]) => `${v} ${RESOURCES[k].label}`).join('、'); return { ok: false, msg: `資源不足:需要 ${need}。` }; } this.spend(def.cost); t.buildings.push(type); this.spendAction(); this.addLog(`在 ${t.name} 建成${def.label}。`, 'player'); this.commit('build', { tid, type }); return { ok: true, tid, type, left: this.state.actionsLeft }; } // ── Diplomacy ───────────────────────────────────────────────────────────── relation(a, b) { const [fa, fb] = a < b ? [a, b] : [b, a]; return this.state.diplomacy.find((r) => r.factionA === fa && r.factionB === fb) || null; } relationStatus(a, b) { return this.relation(a, b)?.status || 'neutral'; } setRelation(a, b, status, turns, initiatedBy) { const [fa, fb] = a < b ? [a, b] : [b, a]; const idx = this.state.diplomacy.findIndex((r) => r.factionA === fa && r.factionB === fb); const rel = { factionA: fa, factionB: fb, status, turnsRemaining: turns, initiatedBy }; if (idx >= 0) this.state.diplomacy[idx] = rel; else this.state.diplomacy.push(rel); } otherFaction(fid) { const f = this.faction(fid); if (!f || f.id === this.state.playerFactionId) return null; return f; } proposeAlliance(fid) { const f = this.otherFaction(fid); if (!f) return { ok: false, msg: '無效的勢力。' }; if (this.outOfActions) return { ok: false, msg: '本回合已無行動,請結束回合。' }; const status = this.relationStatus(this.state.playerFactionId, fid); if (status === 'allied') return { ok: false, msg: '你們已是同盟。' }; let chance = (ACCEPT_RATES[f.personality] || [0.5])[0]; if (f.territories.length <= 1) chance += 0.3; this.spendAction(); if (Math.random() < chance) { this.setRelation(this.state.playerFactionId, fid, 'allied', -1, this.state.playerFactionId); this.addLog(`${f.name} 接受你的同盟提案。`, 'diplomacy'); this.commit('diplomacy'); return { ok: true, accepted: true, msg: `${f.name} 接受你的同盟提案。` }; } this.addLog(`${f.name} 拒絕你的同盟提案。`, 'diplomacy'); this.commit('diplomacy'); return { ok: true, accepted: false, msg: `${f.name} 拒絕了你的提案。` }; } proposePeace(fid) { const f = this.otherFaction(fid); if (!f) return { ok: false, msg: '無效的勢力。' }; if (this.outOfActions) return { ok: false, msg: '本回合已無行動,請結束回合。' }; const status = this.relationStatus(this.state.playerFactionId, fid); if (status === 'peace') return { ok: false, msg: '你們已處於停戰狀態。' }; if (status === 'allied') return { ok: false, msg: '同盟關係已約束雙方。' }; let chance = (ACCEPT_RATES[f.personality] || [0.5, 0.5])[1]; if (f.territories.length <= 1) chance += 0.3; const turns = 3 + Math.floor(Math.random() * 3); this.spendAction(); if (Math.random() < chance) { this.setRelation(this.state.playerFactionId, fid, 'peace', turns, this.state.playerFactionId); this.addLog(`與 ${f.name} 達成 ${turns} 回合停戰。`, 'diplomacy'); this.commit('diplomacy'); return { ok: true, accepted: true, turns, msg: `${f.name} 同意停戰 ${turns} 回合。` }; } this.addLog(`${f.name} 拒絕停戰。`, 'diplomacy'); this.commit('diplomacy'); return { ok: true, accepted: false, msg: `${f.name} 拒絕停戰。` }; } proposeTrade(fid, giveRes, giveAmount, getRes) { const f = this.otherFaction(fid); if (!f) return { ok: false, msg: '無效的勢力。' }; if (this.outOfActions) return { ok: false, msg: '本回合已無行動,請結束回合。' }; if (!['gold', 'food', 'wood', 'stone'].includes(giveRes) || !['gold', 'food', 'wood', 'stone'].includes(getRes)) { return { ok: false, msg: '無效的資源。' }; } if (giveRes === getRes) return { ok: false, msg: '請選擇兩種不同的資源。' }; giveAmount = Math.floor(giveAmount); if (!giveAmount || giveAmount < 1) return { ok: false, msg: '至少提供 1 單位資源。' }; if (this.player[giveRes] < giveAmount) return { ok: false, msg: `你只有 ${this.player[giveRes]} ${RESOURCES[giveRes].label}。` }; const chance = (ACCEPT_RATES[f.personality] || [0.5, 0.5, 0.5])[2]; const markup = TRADE_MARKUP[f.personality] || 1; const getAmount = Math.max(1, Math.round(giveAmount / markup)); if (f[getRes] < getAmount) return { ok: false, msg: `${f.name} 沒有足夠的${RESOURCES[getRes].label}。` }; this.spendAction(); if (Math.random() < chance) { this.player[giveRes] -= giveAmount; this.player[getRes] += getAmount; f[giveRes] += giveAmount; f[getRes] -= getAmount; this.addLog(`以 ${giveAmount} ${RESOURCES[giveRes].label} 向 ${f.name} 換得 ${getAmount} ${RESOURCES[getRes].label}。`, 'diplomacy'); this.commit('diplomacy'); return { ok: true, accepted: true, getAmount, msg: `${f.name} 接受交易:以 ${giveAmount} ${RESOURCES[giveRes].label} 換取 ${getAmount} ${RESOURCES[getRes].label}。` }; } this.addLog(`${f.name} 拒絕你的貿易提案。`, 'diplomacy'); this.commit('diplomacy'); return { ok: true, accepted: false, getAmount: 0, msg: `${f.name} 拒絕了這筆交易。` }; } getRelations() { return this.state.diplomacy.filter((r) => r.factionA === this.state.playerFactionId || r.factionB === this.state.playerFactionId); } relationWith(fid) { return this.relation(this.state.playerFactionId, fid); } // ── Turn cycle ──────────────────────────────────────────────────────────── endTurn() { if (this.state.isOver) return null; const report = { turn: this.state.turn, income: null, upkeep: null, net: null, aiLogs: [], diploLogs: [], starvation: [], playerBefore: null }; const player = this.player; // income & upkeep for every faction for (const f of Object.values(this.state.factions)) { if (f.territories.length === 0) continue; const inc = this.income(f.id); const up = this.upkeep(f.id); f.gold += inc.gold - up.gold; f.food += inc.food - up.food; f.wood += inc.wood - up.wood; f.stone += inc.stone - up.stone; if (f.food < 0) { f.food = 0; if (f.id === this.state.playerFactionId) report.starvation.push('糧倉已空,軍隊吃掉了最後的存糧。'); } if (f.gold < 0) f.gold = 0; if (f.id === this.state.playerFactionId) { report.income = inc; report.upkeep = up; report.net = { gold: inc.gold - up.gold, food: inc.food - up.food, wood: inc.wood, stone: inc.stone }; report.playerBefore = { gold: player.gold, food: player.food }; } } this.addLog(`第 ${this.state.turn} 回合結束。國庫收取 ${report.income.gold} 金幣、${report.income.food} 糧食、${report.income.wood} 木材、${report.income.stone} 石料。`, 'turn'); // AI factions act for (const f of Object.values(this.state.factions)) { if (f.id === this.state.playerFactionId) continue; if (f.territories.length === 0) continue; const logs = this.runAI(f); report.aiLogs.push(...logs); } // diplomacy countdown for (let i = this.state.diplomacy.length - 1; i >= 0; i--) { const rel = this.state.diplomacy[i]; if (rel.status === 'peace' && rel.turnsRemaining > 0) { rel.turnsRemaining -= 1; if (rel.turnsRemaining <= 0) { const a = this.faction(rel.factionA)?.name || rel.factionA; const b = this.faction(rel.factionB)?.name || rel.factionB; this.state.diplomacy.splice(i, 1); report.diploLogs.push(`${a} 與 ${b} 的停戰協議已到期。`); } } } report.aiLogs.forEach((l) => this.addLog(l, 'ai')); report.diploLogs.forEach((l) => this.addLog(l, 'diplomacy')); // player elimination if (player.territories.length === 0) { const winner = Object.values(this.state.factions).find((f) => f.territories.length > 0); this.finish(winner?.id || null); this.commit('turn', report); return report; } this.state.turn += 1; this.state.actionsLeft = MAX_ACTIONS; const win = this.checkWinner(); if (win) this.finish(win); for (const f of Object.values(this.state.factions)) this.syncArmies(f.id); this.commit('turn', report); return report; } runAI(faction) { const logs = []; const squadTerr = this.factionTerritories(faction.id)[0]; if (!squadTerr) return logs; // recruit if (faction.gold >= 6 && faction.food >= 4) { const n = Math.min(Math.floor(faction.gold / 3), Math.floor(faction.food / 2), 3); if (n > 0) { faction.gold -= n * 3; faction.food -= n * 2; squadTerr.armies += n; faction.totalArmies += n; logs.push(`${faction.name} 在 ${squadTerr.name} 徵募 ${n} 名士兵。`); } } // desperate alliances break if (faction.territories.length <= 1 && faction.gold < 5 && faction.personality === 'aggressive') { const rels = this.state.diplomacy.filter((r) => (r.factionA === faction.id || r.factionB === faction.id) && r.status === 'allied'); for (const r of rels) { this.state.diplomacy = this.state.diplomacy.filter((x) => x !== r); logs.push(`${faction.name} 在絕境中撕毀同盟。`); } } // attack one adjacent target for (const tid of [...faction.territories]) { const src = this.territory(tid); if (!src || src.armies < 2) continue; for (const adjId of src.adjacentTo) { const target = this.territory(adjId); if (!target || target.owner === faction.id) continue; if (target.owner) { const status = this.relationStatus(faction.id, target.owner); if (status === 'allied' || status === 'peace') continue; } if (src.armies < target.armies && faction.personality !== 'aggressive') continue; const pre = src.armies; const result = this.resolveCombat(pre, target.armies, target.type, target); src.armies = Math.max(0, pre - result.attackerCasualties); faction.totalArmies = Math.max(0, faction.totalArmies - result.attackerCasualties); logs.push(`${faction.name} 自 ${src.name} 進攻 ${target.name}(${pre} 對 ${target.armies})。`); if (result.captured) { const oldOwnerId = target.owner; if (oldOwnerId) { const old = this.faction(oldOwnerId); target.armies = Math.max(0, target.armies - result.defenderCasualties); if (old) { old.territories = old.territories.filter((id) => id !== adjId); old.totalArmies = Math.max(0, old.totalArmies - result.defenderCasualties); if (old.territories.length === 0) { logs.push(`${old.name} 已從地圖上滅亡。`); if (old.id === this.state.playerFactionId) { target.owner = faction.id; target.armies = src.armies; src.armies = 0; faction.territories.push(adjId); this.addLog(`${faction.name} 占領了 ${target.name}。`, 'critical'); return logs; } } } } const survivors = src.armies; src.armies = 0; target.owner = faction.id; target.armies = survivors; faction.territories.push(adjId); logs.push(`${faction.name} 以 ${survivors} 名倖存者占領 ${target.name}。`); } else { target.armies = Math.max(0, target.armies - result.defenderCasualties); if (target.owner) { const old = this.faction(target.owner); if (old) old.totalArmies = Math.max(0, old.totalArmies - result.defenderCasualties); } logs.push(`${faction.name} 對 ${target.name} 的攻勢被擊退。`); } return logs; } } return logs; } checkWinner() { const total = Object.keys(this.state.territories).length; for (const f of Object.values(this.state.factions)) { if (f.territories.length >= total) return f.id; if (f.territories.length > 0) { const owned = Object.values(this.state.territories).filter((t) => t.owner === f.id).length; if (owned >= total) return f.id; } } return null; } finish(winnerId) { if (this.state.isOver) return; this.state.isOver = true; this.state.winner = winnerId; const name = winnerId ? this.faction(winnerId)?.name : null; this.addLog(winnerId === this.state.playerFactionId ? `勝利!${name} 統治了世界。` : `${name || '某個對手'} 征服了世界。`, 'critical'); } // forecast for attack modal forecast(fromId, toId, n) { const from = this.territory(fromId); const to = this.territory(toId); if (!from || !to) return null; const units = n == null ? from.armies : n; const terrainBonus = TERRAIN_BONUS[to.type] ?? 1; const wallBonus = to.buildings.includes('walls') ? 0.3 : 0; const atk = units * 0.8; const def = to.armies * 0.8 * (terrainBonus + wallBonus); const ratio = atk / (def || 1); let label = '絕望'; let tone = 'bad'; if (to.armies <= 0 || ratio >= 2) { label = '壓倒性'; tone = 'great'; } else if (ratio >= 1.2) { label = '有利'; tone = 'good'; } else if (ratio >= 1.0) { label = '勢均力敵'; tone = 'warn'; } else if (ratio >= 0.8) { label = '冒險'; tone = 'warn'; } else { label = '恐將失敗'; tone = 'bad'; } return { ratio, label, tone, atk, def, terrainBonus, wallBonus }; } factionStanding(fid) { const f = this.faction(fid); if (!f) return null; return { territories: f.territories.length, armies: this.factionTerritories(fid).reduce((s, t) => s + t.armies, 0), personality: f.personality, color: f.color, }; } }