diff --git a/src/locales/de.ts b/src/locales/de.ts index 99f33a4..7c09158 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1085,6 +1085,20 @@ export default { }, gmView: { title: 'GM-Kontrollpanel', + presets: 'Vorlagen', + choosePreset: 'Vorlage wählen', + defaultPreset: 'Standardvorlage', + applyPreset: 'Vorlage anwenden', + savePreset: 'Vorlage speichern', + presetName: 'Vorlagenname', + presetNameRequired: 'Bitte geben Sie einen Namen ein', + presetSaved: 'Vorlage gespeichert', + presetApplied: 'Vorlage angewendet', + deletePreset: 'Vorlage löschen', + presetDeleted: 'Vorlage gelöscht', + confirmOverwriteTitle: 'Vorlage existiert bereits', + confirmOverwriteMessage: 'Eine Vorlage mit dem Namen "{name}" existiert bereits. Überschreiben?', + cannotDeleteDefault: 'Standardvorlage kann nicht gelöscht werden', adminOnly: 'Nur Admin', selectPlanet: 'Planet auswählen', choosePlanet: 'Einen Planeten auswählen', @@ -1185,6 +1199,15 @@ export default { } }, diplomacy: { + sort: { + label: 'Sortieren', + ascending: 'Aufsteigend', + descending: 'Absteigend', + reputation: 'Ruf', + planets: 'Planeten', + difficulty: 'Schwierigkeit', + allies: 'Verbündete' + }, title: 'Diplomatie', description: 'Verwalte diplomatische Beziehungen mit NPCs', tabs: { diff --git a/src/locales/en.ts b/src/locales/en.ts index 26b6422..9fa0b90 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1101,6 +1101,20 @@ export default { }, gmView: { title: 'GM Control Panel', + presets: 'Presets', + choosePreset: 'Choose Preset', + defaultPreset: 'Default Preset', + applyPreset: 'Apply Preset', + savePreset: 'Save Preset', + presetName: 'Preset Name', + presetNameRequired: 'Please enter a preset name', + presetSaved: 'Preset saved', + presetApplied: 'Preset applied', + deletePreset: 'Delete Preset', + presetDeleted: 'Preset deleted', + confirmOverwriteTitle: 'Preset Already Exists', + confirmOverwriteMessage: 'Preset with name "{name}" already exists. Overwrite?', + cannotDeleteDefault: 'Cannot delete default preset', adminOnly: 'Admin Only', selectPlanet: 'Select Planet', choosePlanet: 'Choose a planet', @@ -1291,6 +1305,15 @@ export default { npcEliminatedMessage: "You destroyed all of {npcName}'s planets! This faction has been completely wiped out." }, searchPlaceholder: 'Search NPC name...', + sort: { + label: 'Sort', + ascending: 'Ascending', + descending: 'Descending', + reputation: 'Reputation', + planets: 'Planets', + difficulty: 'Difficulty', + allies: 'Allies' + }, // Notification types notificationType: { tradeOffer: 'Trade Offer', diff --git a/src/locales/es-LA.ts b/src/locales/es-LA.ts index 8f3614f..9067712 100644 --- a/src/locales/es-LA.ts +++ b/src/locales/es-LA.ts @@ -1093,6 +1093,20 @@ export default { }, gmView: { title: 'Panel de Control GM', + presets: 'Preajustes', + choosePreset: 'Elegir preajuste', + defaultPreset: 'Preajuste predeterminado', + applyPreset: 'Aplicar preajuste', + savePreset: 'Guardar preajuste', + presetName: 'Nombre del preajuste', + presetNameRequired: 'Ingrese el nombre del preajuste', + presetSaved: 'Preajuste guardado', + presetApplied: 'Preajuste aplicado', + deletePreset: 'Eliminar preajuste', + presetDeleted: 'Preajuste eliminado', + confirmOverwriteTitle: 'El preajuste ya existe', + confirmOverwriteMessage: 'El preajuste con el nombre "{name}" ya existe. ¿Sobrescribir?', + cannotDeleteDefault: 'No se puede eliminar el preajuste predeterminado', adminOnly: 'Solo Administrador', selectPlanet: 'Seleccionar Planeta', choosePlanet: 'Elige un planeta', @@ -1193,6 +1207,15 @@ export default { } }, diplomacy: { + sort: { + label: 'Ordenar', + ascending: 'Ascendente', + descending: 'Descendente', + reputation: 'Reputación', + planets: 'Planetas', + difficulty: 'Dificultad', + allies: 'Aliados' + }, title: 'Diplomacia', description: 'Gestionar relaciones diplomáticas con NPCs', tabs: { diff --git a/src/locales/ja.ts b/src/locales/ja.ts index d149f87..ab3d6dc 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -1111,6 +1111,20 @@ export default { }, gmView: { title: 'GMコントロールパネル', + presets: 'プリセット', + choosePreset: 'プリセット選択', + defaultPreset: 'デフォルト', + applyPreset: '適用', + savePreset: '保存', + presetName: 'プリセット名', + presetNameRequired: 'プリセット名を入力してください', + presetSaved: '保存しました', + presetApplied: '適用しました', + deletePreset: 'プリセット削除', + presetDeleted: 'プリセットを削除しました', + confirmOverwriteTitle: 'プリセットは既に存在します', + confirmOverwriteMessage: 'プリセット名 "{name}" は既に存在します。上書きしますか?', + cannotDeleteDefault: 'デフォルトプリセットは削除できません', adminOnly: '管理者専用', selectPlanet: '惑星を選択', choosePlanet: '惑星を選択してください', @@ -1210,6 +1224,15 @@ export default { } }, diplomacy: { + sort: { + label: '並び替え', + ascending: '昇順', + descending: '降順', + reputation: '評判', + planets: '惑星', + difficulty: '難易度', + allies: '同盟' + }, title: '外交', description: 'NPCとの外交関係を管理', tabs: { diff --git a/src/locales/ko.ts b/src/locales/ko.ts index bd03c41..c4553cd 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -1061,6 +1061,20 @@ export default { }, gmView: { title: 'GM 제어판', + presets: '프리셋', + choosePreset: '프리셋 선택', + defaultPreset: '기본 프리셋', + applyPreset: '프리셋 적용', + savePreset: '프리셋 저장', + presetName: '프리셋 이름', + presetNameRequired: '프리셋 이름을 입력하세요', + presetSaved: '프리셋 저장됨', + presetApplied: '프리셋 적용됨', + deletePreset: '프리셋 삭제', + presetDeleted: '프리셋 삭제됨', + confirmOverwriteTitle: '프리셋이 이미 존재함', + confirmOverwriteMessage: '"{name}" 이름의 프리셋이 이미 존재합니다. 덮어쓰시겠습니까?', + cannotDeleteDefault: '기본 프리셋은 삭제할 수 없습니다', adminOnly: '관리자 전용', selectPlanet: '행성 선택', choosePlanet: '행성을 선택하세요', @@ -1160,6 +1174,15 @@ export default { } }, diplomacy: { + sort: { + label: '정렬', + ascending: '오름차순', + descending: '내림차순', + reputation: '평판', + planets: '행성', + difficulty: '난이도', + allies: '동맹' + }, title: '외교', description: 'NPC와의 외교 관계 관리', tabs: { diff --git a/src/locales/ru.ts b/src/locales/ru.ts index aa3b1fb..8823039 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1087,6 +1087,20 @@ export default { }, gmView: { title: 'Панель управления GM', + presets: 'Предустановки', + choosePreset: 'Выбрать предустановку', + defaultPreset: 'Стандартная', + applyPreset: 'Применить', + savePreset: 'Сохранить', + presetName: 'Название', + presetNameRequired: 'Введите название', + presetSaved: 'Сохранено', + presetApplied: 'Применено', + deletePreset: 'Удалить', + presetDeleted: 'Удалено', + confirmOverwriteTitle: 'Уже существует', + confirmOverwriteMessage: 'Предустановка с именем "{name}" уже существует. Перезаписать?', + cannotDeleteDefault: 'Нельзя удалить стандартную предустановку', adminOnly: 'Только для администратора', selectPlanet: 'Выбрать планету', choosePlanet: 'Выберите планету', @@ -1186,6 +1200,15 @@ export default { } }, diplomacy: { + sort: { + label: 'Сортировка', + ascending: 'По возрастанию', + descending: 'По убыванию', + reputation: 'Репутация', + planets: 'Планеты', + difficulty: 'Сложность', + allies: 'Союзники' + }, title: 'Дипломатия', description: 'Управление дипломатическими отношениями с NPC', tabs: { diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index c05e298..f6e5996 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -1095,6 +1095,20 @@ export default { modifyOfficers: '修改军官', officersDesc: '快速设置军官到期时间', days: '天', + presets: '预设', + choosePreset: '选择预设', + defaultPreset: '默认预设', + applyPreset: '应用预设', + presetName: '预设名称', + savePreset: '保存预设', + presetNameRequired: '请输入预设名称', + presetSaved: '预设保存成功', + presetApplied: '预设应用成功', + deletePreset: '删除预设', + presetDeleted: '预设已删除', + confirmOverwriteTitle: '预设已存在', + confirmOverwriteMessage: '名为 "{name}" 的预设已存在,是否覆盖?', + cannotDeleteDefault: '无法删除默认预设', npcTesting: 'NPC 测试', npcTestingDesc: '测试NPC侦查和攻击行为', selectNPC: '选择NPC', @@ -1259,6 +1273,15 @@ export default { npcEliminatedMessage: '你消灭了{npcName}的所有星球!该势力已被彻底摧毁。' }, searchPlaceholder: '搜索NPC名称...', + sort: { + label: '排序', + ascending: '升序', + descending: '降序', + reputation: '好感度', + planets: '星球数量', + difficulty: '难度', + allies: '盟友数量' + }, // 通知类型 notificationType: { tradeOffer: '贸易提议', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index f1d6fd1..3084b05 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -1080,6 +1080,20 @@ export default { }, gmView: { title: 'GM 管理面板', + presets: '預設', + choosePreset: '選擇預設', + defaultPreset: '預設範本', + applyPreset: '套用預設', + savePreset: '儲存預設', + presetName: '預設名稱', + presetNameRequired: '請輸入預設名稱', + presetSaved: '預設已儲存', + presetApplied: '預設已套用', + deletePreset: '刪除預設', + presetDeleted: '預設已刪除', + confirmOverwriteTitle: '預設已存在', + confirmOverwriteMessage: '名為 "{name}" 的預設已存在,是否覆蓋?', + cannotDeleteDefault: '無法刪除預設範本', adminOnly: '僅管理員', selectPlanet: '選擇星球', choosePlanet: '選擇一個星球', @@ -1179,6 +1193,15 @@ export default { } }, diplomacy: { + sort: { + label: '排序', + ascending: '升序', + descending: '降序', + reputation: '聲望', + planets: '星球', + difficulty: '難度', + allies: '盟友' + }, title: '外交', description: '管理與NPC的外交關係', tabs: { diff --git a/src/logic/battleLogic.ts b/src/logic/battleLogic.ts index 00159af..1cf52b2 100644 --- a/src/logic/battleLogic.ts +++ b/src/logic/battleLogic.ts @@ -2,6 +2,7 @@ import type { Fleet, Resources, BattleResult, Officer, TechnologyType } from '@/ import { DefenseType, OfficerType } from '@/types/game' import { workerManager } from '@/workers/workerManager' import { MOON_CONFIG } from '@/config/gameConfig' +import { generateId } from '@/utils/id' /** * 执行战斗模拟 @@ -66,7 +67,7 @@ export const simulateBattle = async ( // 生成战斗报告 const battleResult: BattleResult = { - id: `battle_${Date.now()}`, + id: generateId('battle'), timestamp: Date.now(), attackerId: '', defenderId: '', diff --git a/src/logic/campaignLogic.ts b/src/logic/campaignLogic.ts index 1f817e5..b16a509 100644 --- a/src/logic/campaignLogic.ts +++ b/src/logic/campaignLogic.ts @@ -19,6 +19,7 @@ import { type ShipType } from '@/types/game' import { MAIN_CAMPAIGN, getAllQuests, getQuestById, getQuestsByChapter } from '@/config/campaignConfig' +import { generateId } from '@/utils/id' import * as resourceLogic from './resourceLogic' /** @@ -515,7 +516,7 @@ export const createQuestNotification = ( ): QuestNotification => { const quest = getQuestById(questId) return { - id: `quest_notification_${Date.now()}`, + id: generateId('quest_notification'), timestamp: Date.now(), questId, questTitleKey: quest?.titleKey || '', diff --git a/src/logic/fleetLogic.ts b/src/logic/fleetLogic.ts index 9d67b3e..1155d17 100644 --- a/src/logic/fleetLogic.ts +++ b/src/logic/fleetLogic.ts @@ -3,6 +3,7 @@ import type { Locale } from '@/locales' import { ShipType, DefenseType, MissionType, BuildingType, OfficerType, TechnologyType, ExpeditionZone } from '@/types/game' import { FLEET_STORAGE_CONFIG, EXPEDITION_ZONES } from '@/config/gameConfig' import { useGameStore } from '@/stores/gameStore' +import { generateId } from '@/utils/id' import * as battleLogic from './battleLogic' import * as moonLogic from './moonLogic' import * as moonValidation from './moonValidation' @@ -61,7 +62,7 @@ export const createFleetMission = ( ): FleetMission => { const now = Date.now() return { - id: `mission_${now}`, + id: generateId('mission'), playerId, originPlanetId, // 深拷贝targetPosition,避免多个任务共享同一个引用 @@ -171,7 +172,7 @@ export const processAttackArrival = async ( ) // 更新战斗报告ID - battleResult.id = `battle_${Date.now()}` + battleResult.id = generateId('battle') battleResult.attackerId = attacker.id battleResult.defenderId = targetPlanet.ownerId || 'unknown' battleResult.attackerPlanetId = mission.originPlanetId @@ -267,7 +268,7 @@ export const processNPCAttackArrival = async ( ) // 更新战斗报告ID和参与者信息 - battleResult.id = `battle_${Date.now()}` + battleResult.id = generateId('battle') battleResult.attackerId = npc.id battleResult.defenderId = defender.id battleResult.attackerPlanetId = mission.originPlanetId @@ -414,7 +415,7 @@ export const processColonizeArrival = ( // 创建新殖民地 const newPlanet: Planet = { - id: `planet_${Date.now()}`, + id: generateId('planet'), name: `${colonyNameTemplate} ${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}`, ownerId: player.id, position: mission.targetPosition, @@ -564,7 +565,7 @@ export const processSpyArrival = ( const wasDetected = Math.random() < detectionChance const spyReport: SpyReport = { - id: `spy_${Date.now()}`, + id: generateId('spy'), timestamp: Date.now(), spyId: attacker.id, targetPlanetId: targetPlanet.id, @@ -1050,7 +1051,7 @@ export const processDestroyArrival = async ( ) // 更新战斗报告 - battleResult.id = `battle_${Date.now()}` + battleResult.id = generateId('battle') battleResult.attackerId = attacker.id battleResult.defenderId = targetPlanet.ownerId || 'unknown' battleResult.attackerPlanetId = mission.originPlanetId diff --git a/src/logic/missileLogic.ts b/src/logic/missileLogic.ts index aa13252..714e0ba 100644 --- a/src/logic/missileLogic.ts +++ b/src/logic/missileLogic.ts @@ -3,8 +3,66 @@ * 处理星际导弹攻击、射程计算、拦截等 */ -import type { Planet, MissileAttack, DefenseType, TechnologyType, Position } from '@/types/game' -import { DefenseType as DefenseTypes } from '@/types/game' +import type { Planet, MissileAttack, DefenseType, TechnologyType, Position, BuildQueueItem } from '@/types/game' +import { DefenseType as DefenseTypes, BuildingType } from '@/types/game' + +/** + * 计算导弹发射井容量 + */ +export const calculateMissileSiloCapacity = (buildings: Partial>): number => { + const siloLevel = buildings[BuildingType.MissileSilo] || 0 + return siloLevel * 10 // 每级存储10枚导弹 +} + +/** + * 计算当前导弹总数 + */ +export const calculateCurrentMissileCount = (defense: Partial>): number => { + const interplanetaryMissiles = defense[DefenseTypes.InterplanetaryMissile] || 0 + const antiBallisticMissiles = defense[DefenseTypes.AntiBallisticMissile] || 0 + return interplanetaryMissiles + antiBallisticMissiles +} + +/** + * 计算建造队列中的导弹总数 + */ +export const calculateQueueMissileCount = (buildQueue: BuildQueueItem[]): number => { + let queueMissileCount = 0 + + for (const item of buildQueue) { + if (item.type === 'defense') { + const defenseType = item.itemType as DefenseType + if (defenseType === DefenseTypes.InterplanetaryMissile || defenseType === DefenseTypes.AntiBallisticMissile) { + queueMissileCount += item.quantity || 0 + } + } + } + + return queueMissileCount +} + +/** + * 检查导弹容量限制 + */ +export const checkMissileSiloLimit = ( + defenseType: DefenseType, + currentDefense: Partial>, + buildings: Partial>, + quantity: number, + buildQueue?: BuildQueueItem[] +): boolean => { + // 只对导弹类型进行检查 + if (defenseType !== DefenseTypes.InterplanetaryMissile && defenseType !== DefenseTypes.AntiBallisticMissile) { + return true + } + + const maxCapacity = calculateMissileSiloCapacity(buildings) + const currentCount = calculateCurrentMissileCount(currentDefense) + const queueCount = buildQueue ? calculateQueueMissileCount(buildQueue) : 0 + const newCount = currentCount + queueCount + quantity + + return newCount <= maxCapacity +} /** * 计算导弹射程(基于脉冲引擎等级) diff --git a/src/logic/moonLogic.ts b/src/logic/moonLogic.ts index 47105d2..df85588 100644 --- a/src/logic/moonLogic.ts +++ b/src/logic/moonLogic.ts @@ -1,6 +1,7 @@ import type { Planet, Resources } from '@/types/game' import { BuildingType, ShipType, DefenseType } from '@/types/game' import { MOON_CONFIG, FLEET_STORAGE_CONFIG } from '@/config/gameConfig' +import { generateId } from '@/utils/id' /** * 计算月球生成概率 @@ -79,7 +80,7 @@ export const tryGenerateMoon = ( // 生成月球 const moon: Planet = { - id: `moon_${Date.now()}`, + id: generateId('moon'), name: `Moon [${planetPosition.galaxy}:${planetPosition.system}:${planetPosition.position}]`, ownerId: playerId, position: planetPosition, diff --git a/src/logic/planetLogic.ts b/src/logic/planetLogic.ts index 08b1aec..6e8cf45 100644 --- a/src/logic/planetLogic.ts +++ b/src/logic/planetLogic.ts @@ -1,6 +1,7 @@ import type { Planet, Resources } from '@/types/game' import { ShipType, DefenseType, BuildingType } from '@/types/game' import { MOON_CONFIG, PLANET_CONFIG, FLEET_STORAGE_CONFIG } from '@/config/gameConfig' +import { generateId } from '@/utils/id' import * as oreDepositLogic from './oreDepositLogic' /** @@ -173,7 +174,7 @@ export const createMoon = ( moonSuffix: string = "'s Moon", diameter?: number ): Planet => { - const moonId = `moon_${Date.now()}` + const moonId = generateId('moon') const moon: Planet = { id: moonId, name: `${parentPlanet.name}${moonSuffix}`, diff --git a/src/logic/shipLogic.ts b/src/logic/shipLogic.ts index 35e36a0..4659d65 100644 --- a/src/logic/shipLogic.ts +++ b/src/logic/shipLogic.ts @@ -156,64 +156,6 @@ export const checkShieldDomeLimit = ( return true } -/** - * 计算导弹发射井容量 - */ -export const calculateMissileSiloCapacity = (buildings: Partial>): number => { - const siloLevel = buildings[BuildingType.MissileSilo] || 0 - return siloLevel * 10 // 每级存储10枚导弹 -} - -/** - * 计算当前导弹总数 - */ -export const calculateCurrentMissileCount = (defense: Partial>): number => { - const interplanetaryMissiles = defense[DefenseType.InterplanetaryMissile] || 0 - const antiBallisticMissiles = defense[DefenseType.AntiBallisticMissile] || 0 - return interplanetaryMissiles + antiBallisticMissiles -} - -/** - * 计算建造队列中的导弹总数 - */ -export const calculateQueueMissileCount = (buildQueue: Array<{ type: string; itemType: string; quantity?: number }>): number => { - let queueMissileCount = 0 - - for (const item of buildQueue) { - if (item.type === 'defense') { - const defenseType = item.itemType as DefenseType - if (defenseType === DefenseType.InterplanetaryMissile || defenseType === DefenseType.AntiBallisticMissile) { - queueMissileCount += item.quantity || 0 - } - } - } - - return queueMissileCount -} - -/** - * 检查导弹容量限制 - */ -export const checkMissileSiloLimit = ( - defenseType: DefenseType, - currentDefense: Partial>, - buildings: Partial>, - quantity: number, - buildQueue?: Array<{ type: string; itemType: string; quantity?: number }> -): boolean => { - // 只对导弹类型进行检查 - if (defenseType !== DefenseType.InterplanetaryMissile && defenseType !== DefenseType.AntiBallisticMissile) { - return true - } - - const maxCapacity = calculateMissileSiloCapacity(buildings) - const currentCount = calculateCurrentMissileCount(currentDefense) - const queueCount = buildQueue ? calculateQueueMissileCount(buildQueue) : 0 - const newCount = currentCount + queueCount + quantity - - return newCount <= maxCapacity -} - /** * 创建舰船建造队列项 */ diff --git a/src/logic/shipValidation.ts b/src/logic/shipValidation.ts index 0a1ba54..58d854e 100644 --- a/src/logic/shipValidation.ts +++ b/src/logic/shipValidation.ts @@ -1,6 +1,7 @@ import type { Planet, Resources, BuildQueueItem, Fleet, Officer } from '@/types/game' import { ShipType, DefenseType, TechnologyType, OfficerType, BuildingType } from '@/types/game' import * as shipLogic from './shipLogic' +import * as missileLogic from './missileLogic' import * as resourceLogic from './resourceLogic' import * as officerLogic from './officerLogic' import * as publicLogic from './publicLogic' @@ -101,7 +102,7 @@ export const validateDefenseBuild = ( } // 导弹发射井容量限制 - if (!shipLogic.checkMissileSiloLimit(defenseType, planet.defense, planet.buildings, quantity, planet.buildQueue)) { + if (!missileLogic.checkMissileSiloLimit(defenseType, planet.defense, planet.buildings, quantity, planet.buildQueue)) { return { valid: false, reason: 'errors.missileSiloLimit' } } diff --git a/src/utils/id.ts b/src/utils/id.ts new file mode 100644 index 0000000..ae96044 --- /dev/null +++ b/src/utils/id.ts @@ -0,0 +1,8 @@ +/** + * 统一生成带前缀的业务ID + * 便于后续集中调整ID规则 + */ +export const generateId = (prefix: string): string => { + const timestamp = Date.now() + return `${prefix}_${timestamp}_${Math.random().toString(36).slice(2, 9)}` +} diff --git a/src/utils/migration.ts b/src/utils/migration.ts index b6053a0..532820b 100644 --- a/src/utils/migration.ts +++ b/src/utils/migration.ts @@ -1,4 +1,18 @@ -import type { Planet, DebrisField, NPC } from '@/types/game' +import type { + AllyDefenseNotification, + DebrisField, + FleetMission, + IncomingFleetAlert, + JointAttackInvite, + MissionReport, + NPC, + NPCActivityNotification, + Planet, + Player, + Position, + SpiedNotification, + SpyReport +} from '@/types/game' import { decryptData, encryptData } from './crypto' import { generatePlanetTemperature } from '@/logic/planetLogic' import pkg from '../../package.json' @@ -8,6 +22,514 @@ import pkg from '../../package.json' * 用于从旧版本数据结构迁移到新版本 */ +type PlanetKind = 'planet' | 'moon' +type RemappedPlanetEntry = { newId: string; name: string } +type DuplicatePlanetKindMap = Map +type DuplicatePlanetPositionMap = Map + +// oldPlanetId -> position -> planet/moon -> remapped target +type DuplicatePlanetIdMap = Map + +interface MigratablePlayer extends Player { + diplomaticRelations?: Record +} + +interface MigratableGameData { + currentPlanetId?: string + player?: MigratablePlayer + npcs?: NPC[] + universePlanets?: Record + debrisFields?: Record +} + +interface PlanetReferenceContext { + position?: Position + isMoon?: boolean + planetName?: string +} + +interface HasTargetPlanetId { + targetPlanetId?: string +} + +interface HasOriginPlanetId { + originPlanetId?: string +} + +interface HasParentPlanetId { + parentPlanetId?: string +} + +interface HasCurrentPlanetId { + currentPlanetId?: string +} + +const getPlanetPositionKey = (position: Position): string => { + return `${position.galaxy}:${position.system}:${position.position}` +} + +const getPlanetKindKey = (isMoon?: boolean): PlanetKind => { + return isMoon ? 'moon' : 'planet' +} + +const getPlanetEntriesFor = ( + planetId: string, + idMap: DuplicatePlanetIdMap, + position?: Position +): DuplicatePlanetKindMap | undefined => { + if (!position) return undefined + + return idMap.get(planetId)?.get(getPlanetPositionKey(position)) +} + +const getEntriesByName = (entries: Iterable, planetName?: string): RemappedPlanetEntry[] => { + if (!planetName) { + return [] + } + + return Array.from(entries).filter(entry => entry.name === planetName) +} + +const getUniqueEntryByName = (entries: Iterable, planetName?: string): RemappedPlanetEntry | undefined => { + const matchedEntries = getEntriesByName(entries, planetName) + if (matchedEntries.length !== 1) { + return undefined + } + + return matchedEntries[0] +} + +const getOnlyEntry = (entries: DuplicatePlanetKindMap): RemappedPlanetEntry | undefined => { + if (entries.size !== 1) { + return undefined + } + + return Array.from(entries.values())[0] +} + +const getEntriesAcrossPositions = (byPosition: DuplicatePlanetPositionMap): RemappedPlanetEntry[] => { + const entries: RemappedPlanetEntry[] = [] + + byPosition.forEach(byKind => { + byKind.forEach(entry => { + entries.push(entry) + }) + }) + + return entries +} + +const buildDuplicatePlanetIdMap = (player: Player): DuplicatePlanetIdMap => { + const planetsByOriginalId = new Map() + + player.planets.forEach(planet => { + let group = planetsByOriginalId.get(planet.id) + if (!group) { + group = [] + planetsByOriginalId.set(planet.id, group) + } + group.push(planet) + }) + + const idMap: DuplicatePlanetIdMap = new Map() + + planetsByOriginalId.forEach((planets, originalId) => { + if (planets.length <= 1) return + + planets.forEach((planet, index) => { + if (index === 0) return + + const newId = `${originalId}_${Math.random().toString(36).substring(2, 9)}` + const positionKey = getPlanetPositionKey(planet.position) + + let byPosition = idMap.get(originalId) + if (!byPosition) { + byPosition = new Map() + idMap.set(originalId, byPosition) + } + + let byKind = byPosition.get(positionKey) + if (!byKind) { + byKind = new Map() + byPosition.set(positionKey, byKind) + } + + byKind.set(getPlanetKindKey(planet.isMoon), { + newId, + name: planet.name + }) + + planet.id = newId + }) + }) + + return idMap +} + +const resolveRemappedPlanetId = ( + planetId: string | undefined, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): string | undefined => { + if (!planetId) return undefined + + const byPosition = idMap.get(planetId) + if (!byPosition) return undefined + + if (context.position) { + const byKind = getPlanetEntriesFor(planetId, idMap, context.position) + if (!byKind) return undefined + + // 只有在位置或名称足够区分目标时才重写引用,避免把旧引用误指到错误星球 + if (context.isMoon !== undefined) { + return byKind.get(getPlanetKindKey(context.isMoon))?.newId + } + + const matchedByName = getUniqueEntryByName(byKind.values(), context.planetName) + if (matchedByName) { + return matchedByName.newId + } + + return getOnlyEntry(byKind)?.newId + } + + if (context.planetName) { + return getUniqueEntryByName(getEntriesAcrossPositions(byPosition), context.planetName)?.newId + } + + return undefined +} + +const getUpdatedPlanetId = ( + currentPlanetId: string | undefined, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): string | undefined => { + const remappedPlanetId = resolveRemappedPlanetId(currentPlanetId, idMap, context) + if (!remappedPlanetId || remappedPlanetId === currentPlanetId) { + return undefined + } + + return remappedPlanetId +} + +const updateTargetPlanetId = ( + target: HasTargetPlanetId, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): boolean => { + const remappedPlanetId = getUpdatedPlanetId(target.targetPlanetId, idMap, context) + if (!remappedPlanetId) { + return false + } + + target.targetPlanetId = remappedPlanetId + return true +} + +const updateOriginPlanetId = ( + target: HasOriginPlanetId, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): boolean => { + const remappedPlanetId = getUpdatedPlanetId(target.originPlanetId, idMap, context) + if (!remappedPlanetId) { + return false + } + + target.originPlanetId = remappedPlanetId + return true +} + +const updateParentPlanetId = ( + target: HasParentPlanetId, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): boolean => { + const remappedPlanetId = getUpdatedPlanetId(target.parentPlanetId, idMap, context) + if (!remappedPlanetId) { + return false + } + + target.parentPlanetId = remappedPlanetId + return true +} + +const updateCurrentPlanetId = ( + target: HasCurrentPlanetId, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): boolean => { + const remappedPlanetId = getUpdatedPlanetId(target.currentPlanetId, idMap, context) + if (!remappedPlanetId) { + return false + } + + target.currentPlanetId = remappedPlanetId + return true +} + +const updateMissionTargetPlanetId = (mission: FleetMission, idMap: DuplicatePlanetIdMap): boolean => { + return updateTargetPlanetId(mission, idMap, { + position: mission.targetPosition, + isMoon: mission.targetIsMoon + }) +} + +const updateSpyReportTargetPlanetId = (report: SpyReport, idMap: DuplicatePlanetIdMap): boolean => { + return updateTargetPlanetId(report, idMap, { + position: report.targetPosition, + planetName: report.targetPlanetName + }) +} + +const updateSpiedNotificationTargetPlanetId = ( + notification: SpiedNotification, + idMap: DuplicatePlanetIdMap +): boolean => { + return updateTargetPlanetId(notification, idMap, { + planetName: notification.targetPlanetName + }) +} + +const updateNPCActivityTargetPlanetId = ( + notification: NPCActivityNotification, + idMap: DuplicatePlanetIdMap +): boolean => { + return updateTargetPlanetId(notification, idMap, { + position: notification.targetPosition, + planetName: notification.targetPlanetName + }) +} + +const updateIncomingAlertTargetPlanetId = ( + alert: IncomingFleetAlert, + idMap: DuplicatePlanetIdMap +): boolean => { + return updateTargetPlanetId(alert, idMap, { + planetName: alert.targetPlanetName + }) +} + +const updateJointAttackTargetPlanetId = ( + invite: JointAttackInvite, + idMap: DuplicatePlanetIdMap +): boolean => { + return updateTargetPlanetId(invite, idMap, { + position: invite.targetPosition + }) +} + +const updateAllyDefenseTargetPlanetId = ( + notification: AllyDefenseNotification, + idMap: DuplicatePlanetIdMap +): boolean => { + return updateTargetPlanetId(notification, idMap, { + planetName: notification.targetPlanetName + }) +} + +const updateMissionReportPlanetIds = (report: MissionReport, idMap: DuplicatePlanetIdMap): boolean => { + let mutated = false + + if (updateOriginPlanetId(report, idMap, { + planetName: report.originPlanetName + })) { + mutated = true + } + + if (updateTargetPlanetId(report, idMap, { + position: report.targetPosition, + planetName: report.targetPlanetName + })) { + mutated = true + } + + if (report.details?.newPlanetId) { + const remappedNewPlanetId = getUpdatedPlanetId(report.details.newPlanetId, idMap, { + position: report.targetPosition, + planetName: report.details.newPlanetName || report.targetPlanetName + }) + + if (remappedNewPlanetId) { + report.details.newPlanetId = remappedNewPlanetId + mutated = true + } + } + + return mutated +} + +const fixPlayerPlanetsAndQueues = (player: Player, idMap: DuplicatePlanetIdMap): boolean => { + let mutated = false + + player.planets.forEach(planet => { + if (planet.isMoon && updateParentPlanetId(planet, idMap, { + position: planet.position, + isMoon: false + })) { + mutated = true + } + + // 等待队列里的 planetId 应始终与所属星球保持一致 + planet.waitingBuildQueue?.forEach(item => { + if (item.planetId && item.planetId !== planet.id) { + item.planetId = planet.id + mutated = true + } + }) + }) + + return mutated +} + +const fixPlayerReferences = ( + player: Player, + data: MigratableGameData, + idMap: DuplicatePlanetIdMap +): boolean => { + let mutated = false + + if (updateCurrentPlanetId(data, idMap)) { + mutated = true + } + + player.fleetMissions?.forEach(mission => { + if (updateMissionTargetPlanetId(mission, idMap)) { + mutated = true + } + }) + + player.spyReports?.forEach(report => { + if (updateSpyReportTargetPlanetId(report, idMap)) { + mutated = true + } + }) + + player.spiedNotifications?.forEach(notification => { + if (updateSpiedNotificationTargetPlanetId(notification, idMap)) { + mutated = true + } + }) + + player.npcActivityNotifications?.forEach(notification => { + if (updateNPCActivityTargetPlanetId(notification, idMap)) { + mutated = true + } + }) + + player.missionReports?.forEach(report => { + if (updateMissionReportPlanetIds(report, idMap)) { + mutated = true + } + }) + + player.incomingFleetAlerts?.forEach(alert => { + if (updateIncomingAlertTargetPlanetId(alert, idMap)) { + mutated = true + } + }) + + player.jointAttackInvites?.forEach(invite => { + if (updateJointAttackTargetPlanetId(invite, idMap)) { + mutated = true + } + }) + + player.allyDefenseNotifications?.forEach(notification => { + if (updateAllyDefenseTargetPlanetId(notification, idMap)) { + mutated = true + } + }) + + return mutated +} + +const fixNpcPlayerSpyReports = (npc: NPC, idMap: DuplicatePlanetIdMap): boolean => { + if (!npc.playerSpyReports) { + return false + } + + let mutated = false + const remappedPlayerSpyReports: Record = {} + + // playerSpyReports 的 key 就是玩家星球 ID,需要和报告内容一起迁移 + Object.entries(npc.playerSpyReports).forEach(([planetId, report]) => { + if (updateSpyReportTargetPlanetId(report, idMap)) { + mutated = true + } + + const remappedPlanetId = getUpdatedPlanetId(planetId, idMap, { + position: report.targetPosition, + planetName: report.targetPlanetName + }) + + if (remappedPlanetId) { + remappedPlayerSpyReports[remappedPlanetId] = report + mutated = true + return + } + + remappedPlayerSpyReports[planetId] = report + }) + + npc.playerSpyReports = remappedPlayerSpyReports + return mutated +} + +const fixNpcReferences = (npcs: NPC[], idMap: DuplicatePlanetIdMap): boolean => { + let mutated = false + + npcs.forEach(npc => { + if (fixNpcPlayerSpyReports(npc, idMap)) { + mutated = true + } + + npc.fleetMissions?.forEach(mission => { + if (updateMissionTargetPlanetId(mission, idMap)) { + mutated = true + } + }) + }) + + return mutated +} + +/** + * 修复玩家星球的重复ID,并同步更新可被可靠识别的旧引用。 + * 缺少位置或名称上下文、无法安全判定归属的旧引用会保留原ID, + * 继续指向保留下来的首个星球,避免把数据误指到错误目标。 + */ +const fixDuplicatePlanetIds = (data: MigratableGameData): boolean => { + const player = data.player + if (!player || !Array.isArray(player.planets) || player.planets.length === 0) { + return false + } + + const idMap = buildDuplicatePlanetIdMap(player) + if (idMap.size === 0) { + return false + } + + // buildDuplicatePlanetIdMap 已经在上一步直接修复了重复星球 ID, + // 只要 idMap 非空,就说明当前迁移已经发生了实际修改。 + let mutated = true + + if (fixPlayerPlanetsAndQueues(player, idMap)) { + mutated = true + } + + if (fixPlayerReferences(player, data, idMap)) { + mutated = true + } + + if (data.npcs && fixNpcReferences(data.npcs, idMap)) { + mutated = true + } + + return mutated +} + /** * 执行数据迁移 * 将旧版本的 universePlanets 和 debrisFields 从 gameStore 迁移到 universeStore @@ -22,13 +544,13 @@ export const migrateGameData = (): void => { if (!oldEncryptedData) return // 尝试解密(如果是加密格式) - let oldData: any + let oldData: MigratableGameData try { - oldData = decryptData(oldEncryptedData) + oldData = decryptData(oldEncryptedData) as MigratableGameData } catch { // 解密失败,可能是新格式(未加密),直接解析 try { - oldData = JSON.parse(oldEncryptedData) + oldData = JSON.parse(oldEncryptedData) as MigratableGameData } catch { return // 无法解析,放弃迁移 } @@ -100,6 +622,11 @@ export const migrateGameData = (): void => { needsSave = true } + // 修复重复的星球ID + if (fixDuplicatePlanetIds(oldData)) { + needsSave = true + } + // 迁移温度数据:为没有温度的星球生成温度 // 玩家星球 if (oldData.player?.planets && Array.isArray(oldData.player.planets)) { @@ -164,10 +691,11 @@ export const migrateGameData = (): void => { // 新版本统一使用 npc.relations[playerId] 存储NPC对玩家的关系 if (oldData.player?.diplomaticRelations && oldData.npcs && Array.isArray(oldData.npcs)) { const playerId = oldData.player.id + const npcs = oldData.npcs const playerRelations = oldData.player.diplomaticRelations as Record Object.entries(playerRelations).forEach(([npcId, relation]) => { - const npc = oldData.npcs.find((n: NPC) => n.id === npcId) + const npc = npcs.find((n: NPC) => n.id === npcId) if (npc) { if (!npc.relations) { npc.relations = {} diff --git a/src/views/DefenseView.vue b/src/views/DefenseView.vue index 9555adf..cb006b5 100644 --- a/src/views/DefenseView.vue +++ b/src/views/DefenseView.vue @@ -190,6 +190,7 @@ import * as publicLogic from '@/logic/publicLogic' import * as shipValidation from '@/logic/shipValidation' import * as shipLogic from '@/logic/shipLogic' + import * as missileLogic from '@/logic/missileLogic' import * as gameLogic from '@/logic/gameLogic' import * as waitingQueueLogic from '@/logic/waitingQueueLogic' import * as officerLogic from '@/logic/officerLogic' @@ -204,12 +205,12 @@ // 导弹容量相关计算 const missileSiloCapacity = computed(() => { if (!planet.value) return 0 - return shipLogic.calculateMissileSiloCapacity(planet.value.buildings) + return missileLogic.calculateMissileSiloCapacity(planet.value.buildings) }) const currentMissileCount = computed(() => { if (!planet.value) return 0 - return shipLogic.calculateCurrentMissileCount(planet.value.defense) + return missileLogic.calculateCurrentMissileCount(planet.value.defense) }) // AlertDialog 状态 diff --git a/src/views/DiplomacyView.vue b/src/views/DiplomacyView.vue index 1a44377..fb4dc33 100644 --- a/src/views/DiplomacyView.vue +++ b/src/views/DiplomacyView.vue @@ -181,10 +181,37 @@ - -
- - + +
+ +
+ + +
+ + +
+ + + +
@@ -379,6 +406,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' + import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue + } from '@/components/ui/select' import { Dialog, DialogDescription, DialogTitle } from '@/components/ui/dialog' import ScrollableDialogContent from '@/components/ui/dialog/ScrollableDialogContent.vue' import { @@ -393,7 +427,7 @@ import NpcRelationCard from '@/components/npc/NpcRelationCard.vue' import NpcRelationRow from '@/components/npc/NpcRelationRow.vue' import { RelationStatus } from '@/types/game' - import type { DiplomaticRelation } from '@/types/game' + import type { DiplomaticRelation, NPC } from '@/types/game' import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic' import { Search, @@ -403,10 +437,13 @@ Swords, Activity, LayoutGrid, - List + List, + ArrowUpDown } from 'lucide-vue-next' import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty' + type NPCSortBy = 'reputation' | 'planets' | 'difficulty' | 'allies' + const route = useRoute() const gameStore = useGameStore() const npcStore = useNPCStore() @@ -435,6 +472,56 @@ // 搜索功能 const searchQuery = ref('') + // 排序状态 + const sortBy = ref('reputation') + const sortOrder = ref<'asc' | 'desc'>('desc') + + const assertNever = (value: never): never => { + throw new Error(`Unexpected NPC sort type: ${value}`) + } + + // 排序函数 + const sortNpcs = (npcs: NPC[], predicate: (npc: NPC) => boolean = () => true) => { + return npcs.filter(predicate).sort((a, b) => { + let valA = 0 + let valB = 0 + + switch (sortBy.value) { + case 'reputation': + valA = getRelation(a.id)?.reputation || 0 + valB = getRelation(b.id)?.reputation || 0 + break + case 'planets': + valA = a.planets.length + valB = b.planets.length + break + case 'difficulty': + // 简单=1, 普通=2, 困难=3 + // eslint-disable-next-line no-case-declarations + const getDifficultyVal = (diff: string) => { + if (diff === 'hard') return 3 + if (diff === 'medium') return 2 + return 1 + } + valA = a.difficultyLevel || getDifficultyVal(a.difficulty) + valB = b.difficultyLevel || getDifficultyVal(b.difficulty) + break + case 'allies': + valA = a.allies?.length || 0 + valB = b.allies?.length || 0 + break + default: + return assertNever(sortBy.value) + } + + if (sortOrder.value === 'asc') { + return valA - valB + } else { + return valB - valA + } + }) + } + // NPC诊断功能 const npcDiagnosticOpen = ref(false) const npcDiagnostics = ref([]) @@ -620,34 +707,34 @@ } // 搜索过滤函数 - const matchesSearch = (npc: (typeof npcStore.npcs)[0]) => { + const matchesSearch = (npc: NPC) => { if (!searchQuery.value.trim()) return true const query = searchQuery.value.toLowerCase().trim() return npc.name.toLowerCase().includes(query) || npc.id.toLowerCase().includes(query) } // 按关系状态分类NPC(同时应用搜索过滤) - const allNpcs = computed(() => npcStore.npcs.filter(matchesSearch)) + // 先统一排序一次,避免不同标签页在同一批数据上重复排序 + const sortedNpcs = computed(() => sortNpcs(npcStore.npcs, matchesSearch)) + + const allNpcs = computed(() => sortedNpcs.value) const friendlyNpcs = computed(() => { - return npcStore.npcs.filter(npc => { - if (!matchesSearch(npc)) return false + return sortedNpcs.value.filter(npc => { const relation = getRelation(npc.id) return relation?.status === RelationStatus.Friendly }) }) const neutralNpcs = computed(() => { - return npcStore.npcs.filter(npc => { - if (!matchesSearch(npc)) return false + return sortedNpcs.value.filter(npc => { const relation = getRelation(npc.id) return !relation || relation.status === RelationStatus.Neutral }) }) const hostileNpcs = computed(() => { - return npcStore.npcs.filter(npc => { - if (!matchesSearch(npc)) return false + return sortedNpcs.value.filter(npc => { const relation = getRelation(npc.id) return relation?.status === RelationStatus.Hostile }) diff --git a/src/views/FleetView.vue b/src/views/FleetView.vue index b9c5dd5..ad20566 100644 --- a/src/views/FleetView.vue +++ b/src/views/FleetView.vue @@ -642,6 +642,7 @@ import * as diplomaticLogic from '@/logic/diplomaticLogic' import * as gameLogic from '@/logic/gameLogic' import * as moonLogic from '@/logic/moonLogic' + import { generateId } from '@/utils/id' const route = useRoute() const gameStore = useGameStore() @@ -934,7 +935,7 @@ // 生成唯一ID const generatePresetId = (): string => { - return `preset_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + return generateId('fleet_preset') } // 保存当前配置为预设 diff --git a/src/views/GMView.vue b/src/views/GMView.vue index 7e4eed8..e738b3d 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -79,6 +79,44 @@ + + + + {{ t('gmView.presets') || 'Presets' }} + + +
+
+ + + +
+
+ + +
+
+
+
+ {{ t(section.titleKey) }} @@ -213,6 +251,22 @@ + + + + + {{ t('gmView.confirmOverwriteTitle') || 'Preset Already Exists' }} + + {{ t('gmView.confirmOverwriteMessage', { name: pendingPresetToOverwrite?.name || '' }) || `Preset with name "${pendingPresetToOverwrite?.name}" already exists. Overwrite?` }} + + + + {{ t('common.cancel') }} + {{ t('common.confirm') }} + + + + @@ -263,7 +317,286 @@ import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game' import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic' import * as publicLogic from '@/logic/publicLogic' - import { Home } from 'lucide-vue-next' + import { calculateMaxFleetStorage } from '@/logic/fleetStorageLogic' + import { calculateMissileSiloCapacity } from '@/logic/missileLogic' + import { generateId } from '@/utils/id' + import { Home, Trash2 } from 'lucide-vue-next' + + // --- 预设系统 --- + interface GMPreset { + id: string + name: string + values: Record + } + + type GMSectionTabValue = 'buildings' | 'research' | 'ships' | 'defense' | 'officers' + type GMPresetSectionKey = Exclude + + type GMSection = { + tabValue: GMSectionTabValue + titleKey: string + descKey: string + items: string[] + max?: number + placeholder?: string + buttons: { label: string; value: number }[] + getItemName: (item: string) => string + getValue: (item: string) => number + setValue: (item: string, val: number) => void + onButtonClick: (item: string, val: number) => void + } + + type GMPresetSection = GMSection & { + tabValue: GMPresetSectionKey + } + + type GMPresetNameMap = Record + type GMSelectedPresetMap = Record + type GMCustomPresetMap = Record + + interface PendingPresetOverwrite { + section: GMPresetSection + name: string + values: Record + existingIndex: number + } + + // 校验预设结构,避免历史脏数据污染当前视图 + const isGMPreset = (value: unknown): value is GMPreset => { + if (typeof value !== 'object' || value === null) { + return false + } + + const preset = value as Partial + return typeof preset.id === 'string' && typeof preset.name === 'string' && typeof preset.values === 'object' && preset.values !== null + } + + // 只有建筑/科技/舰船/防御页支持预设 + const isPresettableSection = (section: GMSection): section is GMPresetSection => { + return section.tabValue !== 'officers' + } + + const presetOverwriteDialogOpen = ref(false) + const pendingPresetToOverwrite = ref(null) + + const getPresets = (type: GMPresetSectionKey): GMPreset[] => { + const key = `gm_presets_${type}` + const data = localStorage.getItem(key) + if (!data) { + return [] + } + + try { + // 兼容旧版本或手动修改导致的损坏数据,避免页面因解析失败崩溃 + const parsed = JSON.parse(data) + if (!Array.isArray(parsed)) { + localStorage.removeItem(key) + return [] + } + + const presets = parsed.filter(isGMPreset) + // 过滤掉结构不完整的预设,并顺手回写清理后的结果 + if (presets.length !== parsed.length) { + localStorage.setItem(key, JSON.stringify(presets)) + } + + return presets + } catch { + localStorage.removeItem(key) + return [] + } + } + + const savePresets = (type: GMPresetSectionKey, presets: GMPreset[]) => { + localStorage.setItem(`gm_presets_${type}`, JSON.stringify(presets)) + } + + const presetNames = ref({ + buildings: '', + research: '', + ships: '', + defense: '' + }) + + const selectedPresets = ref({ + buildings: 'default', + research: 'default', + ships: 'default', + defense: 'default' + }) + + const customPresets = ref({ + buildings: getPresets('buildings'), + research: getPresets('research'), + ships: getPresets('ships'), + defense: getPresets('defense') + }) + + const handleSavePreset = (section: GMSection) => { + if (!isPresettableSection(section)) return + + const name = presetNames.value[section.tabValue]?.trim() + if (!name) { + toast.error(t('gmView.presetNameRequired') || '请输入预设名称') + return + } + + const values: Record = {} + section.items.forEach((item: string) => { + values[item] = section.getValue(item) + }) + + // 检查是否存在同名预设 + const presets = customPresets.value[section.tabValue] + const existingIndex = presets.findIndex(p => p.name === name) + + if (existingIndex !== -1) { + pendingPresetToOverwrite.value = { + section, + name, + values, + existingIndex + } + presetOverwriteDialogOpen.value = true + return + } + + const newPreset: GMPreset = { + id: generateId('gm_preset'), + name, + values + } + + presets.push(newPreset) + savePresets(section.tabValue, presets) + presetNames.value[section.tabValue] = '' + selectedPresets.value[section.tabValue] = newPreset.id + toast.success(t('gmView.presetSaved') || '预设保存成功') + } + + const handleConfirmOverwrite = () => { + if (!pendingPresetToOverwrite.value) return + + const { section, values, existingIndex } = pendingPresetToOverwrite.value + + const presets = customPresets.value[section.tabValue] + + if (presets[existingIndex]) { + // 更新现有预设的值,保持ID不变 + presets[existingIndex].values = values + + savePresets(section.tabValue, presets) + + presetNames.value[section.tabValue] = '' + selectedPresets.value[section.tabValue] = presets[existingIndex].id + + toast.success(t('gmView.presetSaved') || '预设保存成功') + } + + presetOverwriteDialogOpen.value = false + pendingPresetToOverwrite.value = null + } + + const handleDeletePreset = (section: GMSection) => { + if (!isPresettableSection(section)) return + + const presetId = selectedPresets.value[section.tabValue] + if (!presetId || presetId === 'default') { + toast.error(t('gmView.cannotDeleteDefault') || '无法删除默认预设') + return + } + + const presets = customPresets.value[section.tabValue] + const index = presets.findIndex(p => p.id === presetId) + + if (index !== -1) { + presets.splice(index, 1) + savePresets(section.tabValue, presets) + selectedPresets.value[section.tabValue] = 'default' + toast.success(t('gmView.presetDeleted') || '预设已删除') + } + } + + const handleApplyPreset = (section: GMSection) => { + if (!isPresettableSection(section)) return + + const presetId = selectedPresets.value[section.tabValue] + if (!presetId) return + + if (presetId === 'default') { + if (section.tabValue === 'buildings') { + const explicitMax: Record = { + [BuildingType.NaniteFactory]: 10, + [BuildingType.MissileSilo]: 10, + [BuildingType.JumpGate]: 5, + [BuildingType.PlanetDestroyerFactory]: 3, + [BuildingType.GeoResearchStation]: 10, + [BuildingType.DeepDrillingFacility]: 10, + [BuildingType.University]: 10 + } + section.items.forEach((item: string) => { + section.setValue(item, explicitMax[item] || 50) + }) + } else if (section.tabValue === 'research') { + const explicitMax: Record = { + [TechnologyType.ComputerTechnology]: 10, + [TechnologyType.GravitonTechnology]: 1, + [TechnologyType.PlanetDestructionTech]: 10, + [TechnologyType.MiningTechnology]: 15, + [TechnologyType.IntergalacticResearchNetwork]: 10, + [TechnologyType.MineralResearch]: 20, + [TechnologyType.CrystalResearch]: 20, + [TechnologyType.FuelResearch]: 20 + } + section.items.forEach((item: string) => { + section.setValue(item, explicitMax[item] || 100) + }) + } else if (section.tabValue === 'ships') { + if (!selectedPlanet.value) return + // 某些过滤场景下舰船列表可能为空,避免平均分配时除以 0 + if (!section.items.length) return + + // 重新计算最大舰队仓储,确保数据是最新的 + const maxStorage = calculateMaxFleetStorage(selectedPlanet.value, gameStore.player.technologies) + + // 将总容量平均分配给每种舰船 + const storagePerShip = maxStorage / section.items.length + + section.items.forEach(item => { + const usage = SHIPS.value[item as ShipType]?.storageUsage || 1 + // 如果 usage 为 0 (如某些特殊单位),则给予一个默认数量,或者跳过 + if (usage <= 0) { + section.setValue(item, 100) // 防止除以0,给予固定值 + } else { + section.setValue(item, Math.floor(storagePerShip / usage)) + } + }) + } else if (section.tabValue === 'defense') { + if (!selectedPlanet.value) return + const missileCapacity = calculateMissileSiloCapacity(selectedPlanet.value.buildings) + const defaultMissileCount = Math.floor(missileCapacity / 2) + + section.items.forEach((item: string) => { + // 两种导弹都占用1格空间,默认各分配一半容量 + if (item === DefenseType.AntiBallisticMissile || item === DefenseType.InterplanetaryMissile) { + section.setValue(item, defaultMissileCount) + } else { + section.setValue(item, 10000) + } + }) + } + toast.success(t('gmView.presetApplied') || '默认预设应用成功') + } else { + const customPreset = customPresets.value[section.tabValue].find((p: GMPreset) => p.id === presetId) + if (customPreset) { + Object.entries(customPreset.values).forEach(([k, v]) => { + section.setValue(k, v as number) + }) + toast.success(t('gmView.presetApplied') || '预设应用成功') + } + } + } + // --- 预设系统结束 --- const router = useRouter() const gameStore = useGameStore() @@ -281,7 +614,8 @@ router.push('/') } - const selectedPlanetId = ref(gameStore.player.planets[0]?.id || '') + // 默认选中当前正在游玩的星球 + const selectedPlanetId = ref(gameStore.currentPlanetId || gameStore.player.planets[0]?.id || '') const officerDays = ref>({} as Record) const selectedNPCId = ref(npcStore.npcs[0]?.id || '') const targetPlanetIndex = ref('0') @@ -336,22 +670,6 @@ } } - // GM编辑区块配置 - 统一管理建筑/科技/舰船/防御/军官 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - type GMSection = { - tabValue: string - titleKey: string - descKey: string - items: string[] - max?: number - placeholder?: string - buttons: { label: string; value: number }[] - getItemName: (item: any) => string - getValue: (item: any) => number - setValue: (item: any, val: number) => void - onButtonClick: (item: any, val: number) => void - } - const gmSections = computed(() => [ { tabValue: 'buildings', @@ -364,17 +682,17 @@ { label: 'Lv 10', value: 10 }, { label: 'Lv 30', value: 30 } ], - getItemName: (item: BuildingType) => BUILDINGS.value[item].name, - getValue: (item: BuildingType) => selectedPlanet.value?.buildings[item] || 0, - setValue: (item: BuildingType, val: number) => { + getItemName: item => BUILDINGS.value[item as BuildingType].name, + getValue: item => selectedPlanet.value?.buildings[item as BuildingType] || 0, + setValue: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.buildings[item] = val + selectedPlanet.value.buildings[item as BuildingType] = val updatePlayerPoints() } }, - onButtonClick: (item: BuildingType, val: number) => { + onButtonClick: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.buildings[item] = val + selectedPlanet.value.buildings[item as BuildingType] = val updatePlayerPoints() } } @@ -390,14 +708,14 @@ { label: 'Lv 10', value: 10 }, { label: 'Lv 20', value: 20 } ], - getItemName: (item: TechnologyType) => TECHNOLOGIES.value[item].name, - getValue: (item: TechnologyType) => gameStore.player.technologies[item] || 0, - setValue: (item: TechnologyType, val: number) => { - gameStore.player.technologies[item] = val + getItemName: item => TECHNOLOGIES.value[item as TechnologyType].name, + getValue: item => gameStore.player.technologies[item as TechnologyType] || 0, + setValue: (item, val) => { + gameStore.player.technologies[item as TechnologyType] = val updatePlayerPoints() }, - onButtonClick: (item: TechnologyType, val: number) => { - gameStore.player.technologies[item] = val + onButtonClick: (item, val) => { + gameStore.player.technologies[item as TechnologyType] = val updatePlayerPoints() } }, @@ -412,17 +730,17 @@ { label: '+100', value: 100 }, { label: '+1K', value: 1000 } ], - getItemName: (item: ShipType) => SHIPS.value[item].name, - getValue: (item: ShipType) => selectedPlanet.value?.fleet[item] || 0, - setValue: (item: ShipType, val: number) => { + getItemName: item => SHIPS.value[item as ShipType].name, + getValue: item => selectedPlanet.value?.fleet[item as ShipType] || 0, + setValue: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.fleet[item] = val + selectedPlanet.value.fleet[item as ShipType] = val updatePlayerPoints() } }, - onButtonClick: (item: ShipType, val: number) => { + onButtonClick: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.fleet[item] = (selectedPlanet.value.fleet[item] || 0) + val + selectedPlanet.value.fleet[item as ShipType] = (selectedPlanet.value.fleet[item as ShipType] || 0) + val updatePlayerPoints() } } @@ -438,17 +756,17 @@ { label: '+100', value: 100 }, { label: '+1K', value: 1000 } ], - getItemName: (item: DefenseType) => DEFENSES.value[item].name, - getValue: (item: DefenseType) => selectedPlanet.value?.defense[item] || 0, - setValue: (item: DefenseType, val: number) => { + getItemName: item => DEFENSES.value[item as DefenseType].name, + getValue: item => selectedPlanet.value?.defense[item as DefenseType] || 0, + setValue: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.defense[item] = val + selectedPlanet.value.defense[item as DefenseType] = val updatePlayerPoints() } }, - onButtonClick: (item: DefenseType, val: number) => { + onButtonClick: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.defense[item] = (selectedPlanet.value.defense[item] || 0) + val + selectedPlanet.value.defense[item as DefenseType] = (selectedPlanet.value.defense[item as DefenseType] || 0) + val updatePlayerPoints() } } @@ -465,27 +783,28 @@ { label: `30${t('gmView.days')}`, value: 30 }, { label: `365${t('gmView.days')}`, value: 365 } ], - getItemName: (item: OfficerType) => OFFICERS.value[item].name, - getValue: (item: OfficerType) => officerDays.value[item] || 0, - setValue: (item: OfficerType, val: number) => { - officerDays.value[item] = val + getItemName: item => OFFICERS.value[item as OfficerType].name, + getValue: item => officerDays.value[item as OfficerType] || 0, + setValue: (item, val) => { + officerDays.value[item as OfficerType] = val }, - onButtonClick: (item: OfficerType, days: number) => { - officerDays.value[item] = days + onButtonClick: (item, days) => { + const officerType = item as OfficerType + officerDays.value[officerType] = days const now = Date.now() const expiresAt = now + days * 24 * 60 * 60 * 1000 - if (!gameStore.player.officers[item]) { - gameStore.player.officers[item] = { - type: item, + if (!gameStore.player.officers[officerType]) { + gameStore.player.officers[officerType] = { + type: officerType, active: true, hiredAt: now, expiresAt: expiresAt } } else { - gameStore.player.officers[item].expiresAt = expiresAt - gameStore.player.officers[item].active = true - if (!gameStore.player.officers[item].hiredAt) { - gameStore.player.officers[item].hiredAt = now + gameStore.player.officers[officerType].expiresAt = expiresAt + gameStore.player.officers[officerType].active = true + if (!gameStore.player.officers[officerType].hiredAt) { + gameStore.player.officers[officerType].hiredAt = now } } } @@ -659,11 +978,13 @@ const maxAllResources = () => { if (!selectedPlanet.value) return - const maxAmount = 1000000000000000000 - selectedPlanet.value.resources.metal = maxAmount - selectedPlanet.value.resources.crystal = maxAmount - selectedPlanet.value.resources.deuterium = maxAmount - selectedPlanet.value.resources.darkMatter = maxAmount + // 计算当前星球的资源存储上限 + const capacity = publicLogic.getResourceCapacity(selectedPlanet.value, gameStore.player.officers) + + selectedPlanet.value.resources.metal = capacity.metal + selectedPlanet.value.resources.crystal = capacity.crystal + selectedPlanet.value.resources.deuterium = capacity.deuterium + selectedPlanet.value.resources.darkMatter = capacity.darkMatter toast.success(t('gmView.maxAllResourcesSuccess')) }