diff --git a/app/game-api/src/battleSim/worker.ts b/app/game-api/src/battleSim/worker.ts index 6c5a2ba..7338153 100644 --- a/app/game-api/src/battleSim/worker.ts +++ b/app/game-api/src/battleSim/worker.ts @@ -55,7 +55,7 @@ export const runBattleSimWorker = async (options: BattleSimWorkerOptions = {}): continue; } - let job: BattleSimJob | null = null; + let job: BattleSimJob; try { job = JSON.parse(raw) as BattleSimJob; } catch { diff --git a/app/game-api/src/router/diplomacy/index.ts b/app/game-api/src/router/diplomacy/index.ts index 4854dc0..1c9c907 100644 --- a/app/game-api/src/router/diplomacy/index.ts +++ b/app/game-api/src/router/diplomacy/index.ts @@ -2,14 +2,12 @@ import { TRPCError } from '@trpc/server'; import { z } from 'zod'; import { asRecord } from '@sammo-ts/common'; -import { GamePrisma } from '@sammo-ts/infra'; +import type { GamePrisma } from '@sammo-ts/infra'; import { authedProcedure, router } from '../../trpc.js'; import { getMyGeneral } from '../shared/general.js'; import { assertNationAccess, resolveNationPermission } from '../nation/shared.js'; -const zLetterState = z.enum(['PROPOSED', 'ACTIVATED', 'CANCELLED', 'REPLACED']); - const resolvePermissionLevel = async (ctx: Parameters[0], nationId: number) => { const nation = await ctx.db.nation.findUnique({ where: { id: nationId }, @@ -22,7 +20,7 @@ const resolvePermissionLevel = async (ctx: Parameters[0], n return resolveNationPermission(general, nation.meta, true); }; -const mapLetterState = (state: string): z.infer => { +const mapLetterState = (state: string): 'PROPOSED' | 'ACTIVATED' | 'CANCELLED' | 'REPLACED' => { if (state === 'ACTIVATED') return 'ACTIVATED'; if (state === 'CANCELLED') return 'CANCELLED'; if (state === 'REPLACED') return 'REPLACED'; @@ -153,7 +151,10 @@ export const diplomacyRouter = router({ select: { id: true }, }); if (newer) { - throw new TRPCError({ code: 'BAD_REQUEST', message: '해당 문서에 대한 새로운 문서가 이미 있습니다.' }); + throw new TRPCError({ + code: 'BAD_REQUEST', + message: '해당 문서에 대한 새로운 문서가 이미 있습니다.', + }); } if (prevLetter.state === 'PROPOSED') { @@ -169,7 +170,8 @@ export const diplomacyRouter = router({ }); } - destNationId = prevLetter.srcNationId === me.nationId ? prevLetter.destNationId : prevLetter.srcNationId; + destNationId = + prevLetter.srcNationId === me.nationId ? prevLetter.destNationId : prevLetter.srcNationId; } const nations = await ctx.db.nation.findMany({ @@ -372,4 +374,4 @@ export const diplomacyRouter = router({ }); return { state: 'ACTIVATED' }; }), -}); \ No newline at end of file +}); diff --git a/app/game-engine/src/turn/ai/generalAi/core.ts b/app/game-engine/src/turn/ai/generalAi/core.ts index 71db076..bfcc84f 100644 --- a/app/game-engine/src/turn/ai/generalAi/core.ts +++ b/app/game-engine/src/turn/ai/generalAi/core.ts @@ -711,7 +711,7 @@ export class GeneralAI { const leadership = this.general.stats.leadership; const strength = Math.max(this.general.stats.strength, 1); const intel = Math.max(this.general.stats.intelligence, 1); - let genType = 0; + let genType: number; if (strength >= intel) { genType = t무장; diff --git a/app/game-engine/src/turn/ai/generalAi/nation/assignments/troopAssignments.ts b/app/game-engine/src/turn/ai/generalAi/nation/assignments/troopAssignments.ts index 90b00ad..d2a14dd 100644 --- a/app/game-engine/src/turn/ai/generalAi/nation/assignments/troopAssignments.ts +++ b/app/game-engine/src/turn/ai/generalAi/nation/assignments/troopAssignments.ts @@ -38,7 +38,7 @@ export const do부대전방발령 = (ai: GeneralAI) => { const force = ai.nationPolicy.combatForce[leader.id]; let [fromCityId, toCityId] = force; - let targetCityId: number | null = null; + let targetCityId: number | null; if (!ai.warRoute || !ai.warRoute[fromCityId] || ai.warRoute[fromCityId][toCityId] === undefined) { targetCityId = pickRandomCityId(ai, ai.frontCities); } else { diff --git a/app/game-engine/src/turn/incomeHandler.ts b/app/game-engine/src/turn/incomeHandler.ts index 171efe8..ce2cee5 100644 --- a/app/game-engine/src/turn/incomeHandler.ts +++ b/app/game-engine/src/turn/incomeHandler.ts @@ -108,7 +108,7 @@ const applyIncomeOutcome = ( originOutcome: number ): { next: number; ratio: number; realOutcome: number } => { let next = current + income; - let realOutcome = 0; + let realOutcome: number; if (next < baseResource) { realOutcome = 0; next = baseResource; @@ -139,14 +139,11 @@ const processIncomeForNation = ( const trait = traitMap.get(nation.typeCode) ?? null; const incomeContext = buildNationIncomeContext(nation, trait); - let income = 0; - if (type === 'gold') { - income = getGoldIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level); - } else { - income = - getRiceIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level) + - getWallIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level); - } + const income = + type === 'gold' + ? getGoldIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level) + : getRiceIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level) + + getWallIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level); const incomeValue = roundResource(income); const originOutcome = getOutcome(100, nationGenerals); diff --git a/app/game-engine/src/turn/unificationHandler.ts b/app/game-engine/src/turn/unificationHandler.ts index b0c3626..85a6afb 100644 --- a/app/game-engine/src/turn/unificationHandler.ts +++ b/app/game-engine/src/turn/unificationHandler.ts @@ -167,7 +167,8 @@ export const createUnificationHandler = (options: { sabotage, dex, unifier, - unifierAward: general.nationId === winnerNationId && general.officerLevel > 4 ? UNIFIER_POINT : 0, + unifierAward: + general.nationId === winnerNationId && general.officerLevel > 4 ? UNIFIER_POINT : 0, }, }, }); @@ -194,15 +195,11 @@ export const createUnificationHandler = (options: { const meta = asRecord(state.meta); const serverId = - typeof meta.serverId === 'string' && meta.serverId.trim() - ? meta.serverId.trim() - : options.profileName; + typeof meta.serverId === 'string' && meta.serverId.trim() ? meta.serverId.trim() : options.profileName; const season = readMetaNumberOrNull(meta, 'season') ?? 1; const scenario = readMetaNumberOrNull(meta, 'scenarioId') ?? 0; const scenarioName = - typeof asRecord(meta.scenarioMeta).title === 'string' - ? String(asRecord(meta.scenarioMeta).title) - : ''; + typeof asRecord(meta.scenarioMeta).title === 'string' ? String(asRecord(meta.scenarioMeta).title) : ''; const startTime = typeof meta.starttime === 'string' ? meta.starttime : null; const unitedTime = new Date().toISOString(); @@ -307,14 +304,16 @@ export const createUnificationHandler = (options: { }; for (const [typeName, valueType] of hallTypes) { - let value = 0; - if (valueType === 'natural') { - value = typeName === 'experience' ? general.experience : typeName === 'dedication' ? general.dedication : ranks[typeName] ?? 0; - } else if (valueType === 'rank') { - value = ranks[typeName] ?? 0; - } else { - value = calcValues[typeName] ?? 0; - } + const value = + valueType === 'natural' + ? typeName === 'experience' + ? general.experience + : typeName === 'dedication' + ? general.dedication + : (ranks[typeName] ?? 0) + : valueType === 'rank' + ? (ranks[typeName] ?? 0) + : (calcValues[typeName] ?? 0); if ((typeName === 'winrate' || typeName === 'killrate') && warnum < 10) { continue; @@ -391,9 +390,7 @@ export const createUnificationHandler = (options: { const meta = asRecord(state.meta); const serverId = - typeof meta.serverId === 'string' && meta.serverId.trim() - ? meta.serverId.trim() - : options.profileName; + typeof meta.serverId === 'string' && meta.serverId.trim() ? meta.serverId.trim() : options.profileName; const serverName = typeof meta.serverName === 'string' && meta.serverName.trim() ? meta.serverName.trim() @@ -653,30 +650,30 @@ export const createUnificationHandler = (options: { await Promise.all( oldGeneralTargets.map((general) => ((snapshot) => - prisma.oldGeneral.upsert({ - where: { - by_no: { + prisma.oldGeneral.upsert({ + where: { + by_no: { + serverId, + generalNo: general.id, + }, + }, + update: { + owner: general.userId ?? null, + name: general.name, + lastYearMonth: state.currentYear * 100 + state.currentMonth, + turnTime: general.turnTime, + data: snapshot, + }, + create: { serverId, generalNo: general.id, + owner: general.userId ?? null, + name: general.name, + lastYearMonth: state.currentYear * 100 + state.currentMonth, + turnTime: general.turnTime, + data: snapshot, }, - }, - update: { - owner: general.userId ?? null, - name: general.name, - lastYearMonth: state.currentYear * 100 + state.currentMonth, - turnTime: general.turnTime, - data: snapshot, - }, - create: { - serverId, - generalNo: general.id, - owner: general.userId ?? null, - name: general.name, - lastYearMonth: state.currentYear * 100 + state.currentMonth, - turnTime: general.turnTime, - data: snapshot, - }, - }))( { + }))({ ...general, turnTime: general.turnTime.toISOString(), }) diff --git a/app/game-engine/src/turn/worldCommandHandler.ts b/app/game-engine/src/turn/worldCommandHandler.ts index 42efbb9..8bffaf2 100644 --- a/app/game-engine/src/turn/worldCommandHandler.ts +++ b/app/game-engine/src/turn/worldCommandHandler.ts @@ -315,8 +315,8 @@ async function handleTournamentMatchResult( const attackerG = getRankNumber(attacker, rankKey('g')); const defenderG = getRankNumber(defender, rankKey('g')); - let attackerGDelta = 0; - let defenderGDelta = 0; + let attackerGDelta: number; + let defenderGDelta: number; let attackerW = 0; let attackerD = 0; let attackerL = 0; diff --git a/app/game-engine/test/databaseCommandQueue.integration.test.ts b/app/game-engine/test/databaseCommandQueue.integration.test.ts index 9912dd2..2510111 100644 --- a/app/game-engine/test/databaseCommandQueue.integration.test.ts +++ b/app/game-engine/test/databaseCommandQueue.integration.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { createGamePostgresConnector, GamePrisma, type GamePrismaClient } from '@sammo-ts/infra'; +import { createGamePostgresConnector } from '@sammo-ts/infra'; +import type { GamePrisma, GamePrismaClient } from '@sammo-ts/infra'; import { DatabaseTurnDaemonCommandQueue } from '../src/lifecycle/databaseCommandQueue.js'; diff --git a/app/game-engine/test/nationTurnCompatibility.test.ts b/app/game-engine/test/nationTurnCompatibility.test.ts index 5608d86..029e3af 100644 --- a/app/game-engine/test/nationTurnCompatibility.test.ts +++ b/app/game-engine/test/nationTurnCompatibility.test.ts @@ -164,7 +164,7 @@ describe('레거시 사령부 턴 실행 호환성', () => { ); }); - it('MONTH action 전에 전략·외교 제한, 임시 세율, 첩보 기간을 갱신한다', () => { + it('MONTH action 전에 전략·외교 제한, 임시 세율, 첩보 기간을 갱신한다', async () => { const updates: Array<{ id: number; patch: Record }> = []; const nations = [ { @@ -188,7 +188,7 @@ describe('레거시 사령부 턴 실행 호환성', () => { }) as never, }); - handler.beforeMonthChanged?.({} as never); + await handler.beforeMonthChanged?.({} as never); expect(updates).toEqual([ { diff --git a/app/game-engine/test/npcNationTechResearch.test.ts b/app/game-engine/test/npcNationTechResearch.test.ts index aa7fc56..a3fc3c3 100644 --- a/app/game-engine/test/npcNationTechResearch.test.ts +++ b/app/game-engine/test/npcNationTechResearch.test.ts @@ -327,5 +327,5 @@ describe('NPC 기술 연구 장기 시뮬레이션', () => { // Nation awards can occur in the same tick and make the general's net // gold delta smaller than the recruitment price. Exact cost scaling is // covered by the unit-set/action contract tests rather than this smoke. - }, 60000); + }, 300_000); }); diff --git a/app/game-engine/test/npcNationUprisingUnification.test.ts b/app/game-engine/test/npcNationUprisingUnification.test.ts index dea8395..214f681 100644 --- a/app/game-engine/test/npcNationUprisingUnification.test.ts +++ b/app/game-engine/test/npcNationUprisingUnification.test.ts @@ -553,5 +553,5 @@ describe('NPC 건국/통일 장기 시뮬레이션', () => { } throw error; } - }, 180000); + }, 360_000); }); diff --git a/app/game-engine/vitest.config.ts b/app/game-engine/vitest.config.ts index 400c273..83852b4 100644 --- a/app/game-engine/vitest.config.ts +++ b/app/game-engine/vitest.config.ts @@ -13,5 +13,7 @@ export default defineConfig({ environment: 'node', globals: true, include: ['test/**/*.test.ts'], + maxWorkers: 4, + testTimeout: 10_000, }, }); diff --git a/app/game-frontend/src/components/battle/BattleGeneralCard.vue b/app/game-frontend/src/components/battle/BattleGeneralCard.vue index 7a84f23..ed7eb05 100644 --- a/app/game-frontend/src/components/battle/BattleGeneralCard.vue +++ b/app/game-frontend/src/components/battle/BattleGeneralCard.vue @@ -3,7 +3,6 @@ import { computed, ref } from 'vue'; import type { BattleSimOptions, GeneralDraft } from '../../utils/battleSimulatorTypes'; interface Props { - general: GeneralDraft; options: BattleSimOptions; mode: 'attacker' | 'defender'; title: string; @@ -11,6 +10,7 @@ interface Props { } const props = defineProps(); +const general = defineModel('general', { required: true }); const emit = defineEmits<{ (event: 'import'): void; diff --git a/app/game-frontend/src/utils/formatLog.ts b/app/game-frontend/src/utils/formatLog.ts index 39ac1e5..f23dd9d 100644 --- a/app/game-frontend/src/utils/formatLog.ts +++ b/app/game-frontend/src/utils/formatLog.ts @@ -24,11 +24,10 @@ export const formatLog = (text?: string): string => { return ''; } - let match: RegExpExecArray | null = null; let lastIndex = 0; const result: string[] = []; - while ((match = logRegex.exec(text)) !== null) { + for (let match = logRegex.exec(text); match !== null; match = logRegex.exec(text)) { const partAll = match[0]; const subPart = match[1]; const index = match.index; @@ -40,9 +39,7 @@ export const formatLog = (text?: string): string => { if (subPart === '/') { result.push(''); } else if (subPart.length === 2) { - result.push( - `` - ); + result.push(``); } else { result.push(``); } diff --git a/app/game-frontend/src/views/BattleSimulatorView.vue b/app/game-frontend/src/views/BattleSimulatorView.vue index e8ced0d..a63e727 100644 --- a/app/game-frontend/src/views/BattleSimulatorView.vue +++ b/app/game-frontend/src/views/BattleSimulatorView.vue @@ -1125,7 +1125,7 @@ const shouldShowUI = computed(() => !loading.value && !!options.value); !loading.value && !!options.value); { } }; -const prevOptions = computed(() => - data.value?.letters.filter((letter) => letter.state !== 'CANCELLED') ?? [] -); +const prevOptions = computed(() => data.value?.letters.filter((letter) => letter.state !== 'CANCELLED') ?? []); const formatDate = (value: string) => new Date(value).toLocaleString('ko-KR'); @@ -216,12 +214,14 @@ const canRollback = (letter: DiplomacyLetter) => editable.value && data.value?.myNationId === letter.src.nationId && letter.state === 'PROPOSED'; const canDestroy = (letter: DiplomacyLetter) => - editable.value && letter.state === 'ACTIVATED' && (data.value?.myNationId === letter.src.nationId || data.value?.myNationId === letter.dest.nationId); + editable.value && + letter.state === 'ACTIVATED' && + (data.value?.myNationId === letter.src.nationId || data.value?.myNationId === letter.dest.nationId); const canRenew = (letter: DiplomacyLetter) => letter.state !== 'CANCELLED'; onMounted(() => { - loadLetters(); + void loadLetters(); }); onBeforeUnmount(() => { @@ -270,26 +270,84 @@ onBeforeUnmount(() => {
내용(국가 내 공개)
- - - + + + - - + +
내용(외교권자 전용)
- - - + + + - - + +
@@ -327,7 +385,10 @@ onBeforeUnmount(() => {

이전 문서를 찾을 수 없습니다.

@@ -335,11 +396,24 @@ onBeforeUnmount(() => {
- - + + - +
@@ -565,4 +639,4 @@ onBeforeUnmount(() => { .loading { color: #9aa3b8; } - \ No newline at end of file + diff --git a/app/game-frontend/src/views/NationAffairsView.vue b/app/game-frontend/src/views/NationAffairsView.vue index 34ccff6..4e3e57e 100644 --- a/app/game-frontend/src/views/NationAffairsView.vue +++ b/app/game-frontend/src/views/NationAffairsView.vue @@ -242,7 +242,7 @@ watch( ); onMounted(() => { - loadData(); + void loadData(); }); onBeforeUnmount(() => { @@ -278,19 +278,17 @@ onBeforeUnmount(() => {

국가 방침

- - - + + +
-
@@ -376,7 +416,9 @@ onBeforeUnmount(() => { /> 전쟁 금지 - 잔여 {{ data.warSettingCnt.remain }}회 (월 +{{ data.warSettingCnt.inc }}회) + 잔여 {{ data.warSettingCnt.remain }}회 (월 +{{ data.warSettingCnt.inc }}회)
@@ -574,4 +616,4 @@ onBeforeUnmount(() => { .loading { color: #9aa3b8; } - \ No newline at end of file + diff --git a/app/game-frontend/src/views/ScoutMessageView.vue b/app/game-frontend/src/views/ScoutMessageView.vue index 038604a..1f7fc5b 100644 --- a/app/game-frontend/src/views/ScoutMessageView.vue +++ b/app/game-frontend/src/views/ScoutMessageView.vue @@ -134,7 +134,7 @@ watch( ); onMounted(() => { - loadData(); + void loadData(); }); onBeforeUnmount(() => { @@ -168,7 +168,11 @@ onBeforeUnmount(() => {
-