feat: 타입 정의 개선 및 any 사용 제거

This commit is contained in:
2026-01-05 17:19:32 +00:00
parent 7e58f7bac4
commit cb7e9c832f
17 changed files with 176 additions and 86 deletions
+37 -10
View File
@@ -14,6 +14,18 @@ const buildDb = () => {
const generalTurns = new Map<number, GeneralTurnRow[]>();
const nationTurns = new Map<string, NationTurnRow[]>();
type GeneralTurnFindManyArgs = Parameters<DatabaseClient['generalTurn']['findMany']>[0];
type GeneralTurnDeleteManyArgs = NonNullable<Parameters<DatabaseClient['generalTurn']['deleteMany']>[0]>;
type GeneralTurnCreateManyArgs = NonNullable<Parameters<DatabaseClient['generalTurn']['createMany']>[0]>;
type GeneralTurnCreateManyData = GeneralTurnCreateManyArgs['data'];
type GeneralTurnCreateManyRow = GeneralTurnCreateManyData extends Array<infer Row> ? Row : never;
type NationTurnFindManyArgs = Parameters<DatabaseClient['nationTurn']['findMany']>[0];
type NationTurnDeleteManyArgs = NonNullable<Parameters<DatabaseClient['nationTurn']['deleteMany']>[0]>;
type NationTurnCreateManyArgs = NonNullable<Parameters<DatabaseClient['nationTurn']['createMany']>[0]>;
type NationTurnCreateManyData = NationTurnCreateManyArgs['data'];
type NationTurnCreateManyRow = NationTurnCreateManyData extends Array<infer Row> ? Row : never;
const db = {
worldState: {
findFirst: async () => null,
@@ -28,13 +40,18 @@ const buildDb = () => {
findUnique: async () => null,
},
generalTurn: {
findMany: async ({ where }: any) => generalTurns.get(where.generalId) ?? [],
deleteMany: async ({ where }: any) => {
generalTurns.delete(where.generalId);
findMany: async (args?: GeneralTurnFindManyArgs) => {
const generalId = typeof args?.where?.generalId === 'number' ? args.where.generalId : undefined;
return generalId !== undefined ? (generalTurns.get(generalId) ?? []) : [];
},
deleteMany: async ({ where }: GeneralTurnDeleteManyArgs) => {
if (typeof where.generalId === 'number') {
generalTurns.delete(where.generalId);
}
return {};
},
createMany: async ({ data }: any) => {
const rows = data.map((row: any, index: number) => ({
createMany: async ({ data }: GeneralTurnCreateManyArgs) => {
const rows = data.map((row: GeneralTurnCreateManyRow, index: number) => ({
id: index + 1,
generalId: row.generalId,
turnIdx: row.turnIdx,
@@ -49,13 +66,23 @@ const buildDb = () => {
},
},
nationTurn: {
findMany: async ({ where }: any) => nationTurns.get(`${where.nationId}:${where.officerLevel}`) ?? [],
deleteMany: async ({ where }: any) => {
nationTurns.delete(`${where.nationId}:${where.officerLevel}`);
findMany: async (args?: NationTurnFindManyArgs) => {
const nationId = typeof args?.where?.nationId === 'number' ? args.where.nationId : undefined;
const officerLevel =
typeof args?.where?.officerLevel === 'number' ? args.where.officerLevel : undefined;
if (nationId === undefined || officerLevel === undefined) {
return [];
}
return nationTurns.get(`${nationId}:${officerLevel}`) ?? [];
},
deleteMany: async ({ where }: NationTurnDeleteManyArgs) => {
if (typeof where.nationId === 'number' && typeof where.officerLevel === 'number') {
nationTurns.delete(`${where.nationId}:${where.officerLevel}`);
}
return {};
},
createMany: async ({ data }: any) => {
const rows = data.map((row: any, index: number) => ({
createMany: async ({ data }: NationTurnCreateManyArgs) => {
const rows = data.map((row: NationTurnCreateManyRow, index: number) => ({
id: index + 1,
nationId: row.nationId,
officerLevel: row.officerLevel,
+4 -3
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import type { GameApiContext, GameProfile, WorldStateRow } from '../src/context.js';
import type { DatabaseClient, GameApiContext, GameProfile, WorldStateRow } from '../src/context.js';
import type { RedisConnector } from '@sammo-ts/infra';
import { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.js';
import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js';
import { appRouter } from '../src/router.js';
@@ -43,12 +44,12 @@ const buildContext = (options?: {
},
};
return {
db: db as any,
db: db as unknown as DatabaseClient,
turnDaemon: transport,
battleSim,
profile,
auth: null,
redis: {} as any,
redis: {} as unknown as RedisConnector['client'],
};
};
+4 -2
View File
@@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest';
import { InMemoryGatewaySessionService } from '../src/auth/inMemorySessionService.js';
import { createInMemoryUserRepository } from '../src/auth/inMemoryUserRepository.js';
import { InMemoryOAuthSessionStore } from '../src/auth/oauthSessionStore.js';
import type { KakaoOAuthClient } from '../src/auth/kakaoClient.js';
import { createGatewayApiContext } from '../src/context.js';
import { InMemoryProfileStatusService } from '../src/lobby/profileStatusService.js';
import { appRouter } from '../src/router.js';
import type { GatewayPrismaClient } from '@sammo-ts/infra';
const buildCaller = () => {
const users = createInMemoryUserRepository();
@@ -76,14 +78,14 @@ const buildCaller = () => {
flushPublisher,
gameTokenSecret: 'test-secret',
gameSessionTtlSeconds: 600,
kakaoClient: kakaoClient as any,
kakaoClient: kakaoClient as unknown as KakaoOAuthClient,
oauthSessions,
publicBaseUrl: 'http://localhost',
profiles,
orchestrator,
profileStatus,
requestHeaders: {},
prisma: {} as any,
prisma: {} as unknown as GatewayPrismaClient,
})
);
return { caller, oauthSessions };
+12 -3
View File
@@ -1,15 +1,24 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import type { inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from '@sammo-ts/gateway-api';
import DefaultLayout from '../layouts/DefaultLayout.vue';
import { trpc } from '../utils/trpc';
import { createGameTrpc } from '../utils/gameTrpc';
import type { GameRouter } from '../utils/gameTrpc';
type GatewayRouterOutput = inferRouterOutputs<AppRouter>;
type GameRouterOutput = inferRouterOutputs<GameRouter>;
type MeOutput = GatewayRouterOutput['me'];
type LobbyProfile = GatewayRouterOutput['lobby']['profiles'][number];
type LobbyInfo = GameRouterOutput['lobby']['info'];
const router = useRouter();
const me = ref<any>(null);
const me = ref<MeOutput>(null);
const notice = ref('');
const profiles = ref<any[]>([]);
const profileDetails = ref<Record<string, any>>({});
const profiles = ref<LobbyProfile[]>([]);
const profileDetails = ref<Record<string, LobbyInfo | undefined>>({});
onMounted(async () => {
try {
@@ -13,8 +13,8 @@ export interface TurnCommandSpecBase<TKey extends string = string> {
export interface TurnCommandModule<TSpec extends TurnCommandSpecBase = TurnCommandSpecBase> {
commandSpec: TSpec;
ActionDefinition: new (...args: any[]) => GeneralActionDefinition;
ActionResolver?: new (...args: any[]) => GeneralActionResolver;
CommandResolver?: new (...args: any[]) => any;
ActionDefinition: new (...args: unknown[]) => GeneralActionDefinition;
ActionResolver?: new (...args: unknown[]) => GeneralActionResolver;
CommandResolver?: new (...args: unknown[]) => unknown;
actionContextBuilder?: ActionContextBuilder;
}
@@ -35,7 +35,9 @@ export class ActionDefinition<
if (typeof raw !== 'object' || raw === null) {
return null;
}
const { buyRice, amount } = raw as any;
const record = raw as Record<string, unknown>;
const buyRice = record.buyRice;
const amount = record.amount;
if (typeof buyRice !== 'boolean' || typeof amount !== 'number') {
return null;
}
@@ -0,0 +1,34 @@
export interface WarDexAux {
isAttacker?: boolean;
opposeType?: { armType: number };
}
const isRecord = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object';
export const parseWarDexAux = (aux: unknown): WarDexAux => {
if (!isRecord(aux)) {
return {};
}
const isAttacker = typeof aux.isAttacker === 'boolean' ? aux.isAttacker : undefined;
const opposeRaw = aux.opposeType;
if (!isRecord(opposeRaw)) {
return { isAttacker };
}
const armType = opposeRaw.armType;
if (typeof armType !== 'number') {
return { isAttacker };
}
return { isAttacker, opposeType: { armType } };
};
export const getAuxArmType = (aux: unknown): number | undefined => {
if (!isRecord(aux)) {
return undefined;
}
const armType = aux.armType;
return typeof armType === 'number' ? armType : undefined;
};
@@ -44,12 +44,12 @@ export const traitModule: TraitModule = {
kind: 'war',
getName: () => '견고',
getInfo: () => '[전투] 상대 필살 확률 -20%p, 상대 계략 시도시 성공 확률 -10%p, 부상 없음, 아군 피해 -10%',
onCalcOpposeStat: (_context, statName, value: any, _aux) => {
if (statName === 'warMagicSuccessProb') {
return (value as number) - 0.1;
onCalcOpposeStat: (_context, statName, value, _aux) => {
if (statName === 'warMagicSuccessProb' && typeof value === 'number') {
return value - 0.1;
}
if (statName === 'warCriticalRatio') {
return (value as number) - 0.2;
if (statName === 'warCriticalRatio' && typeof value === 'number') {
return value - 0.2;
}
return value;
},
@@ -4,6 +4,7 @@ import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { WarUnitCity } from '@sammo-ts/logic/war/units.js';
import { getAuxArmType, parseWarDexAux } from './aux.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
@@ -30,8 +31,7 @@ function onCalcStat(
if (statName.startsWith('dex')) {
const myDex = getMetaNumber(context.general.meta, `dex${siegeType}`);
const isAttacker = (aux as any)?.isAttacker;
const opposeType = (aux as any)?.opposeType;
const { isAttacker, opposeType } = parseWarDexAux(aux);
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
return (value as number) + myDex;
@@ -54,10 +54,9 @@ export const traitModule: TraitModule = {
'[군사] 차병 계통 징·모병비 -10%<br>[전투] 성벽 공격 시 대미지 +100%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산',
onCalcDomestic: (_context, turnType, varType, value, aux) => {
if (turnType === '징병' || turnType === '모병') {
if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) {
if ((aux as any).armType === 4) {
return value * 0.9;
}
const armType = getAuxArmType(aux);
if (varType === 'cost' && armType === 4) {
return value * 0.9;
}
}
return value;
@@ -3,6 +3,7 @@ import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/type
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { getAuxArmType, parseWarDexAux } from './aux.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
@@ -33,8 +34,7 @@ function onCalcStat(
if (statName.startsWith('dex')) {
const myDex = getMetaNumber(context.general.meta, `dex${archerType}`);
const isAttacker = (aux as any)?.isAttacker;
const opposeType = (aux as any)?.opposeType;
const { isAttacker, opposeType } = parseWarDexAux(aux);
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
return (value as number) + myDex;
@@ -57,12 +57,11 @@ export const traitModule: TraitModule = {
'[군사] 궁병 계통 징·모병비 -10%<br>[전투] 회피 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산',
onCalcDomestic: (_context, turnType, varType, value, aux) => {
if (turnType === '징병' || turnType === '모병') {
if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) {
// Note: In a real scenario, we should check if aux.armType is archer.
// Since we don't have easy access to config here, we might need to assume legacy ID 2 or similar.
if ((aux as any).armType === 2) {
return value * 0.9;
}
const armType = getAuxArmType(aux);
// Note: In a real scenario, we should check if aux.armType is archer.
// Since we don't have easy access to config here, we might need to assume legacy ID 2 or similar.
if (varType === 'cost' && armType === 2) {
return value * 0.9;
}
}
return value;
@@ -3,6 +3,7 @@ import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/type
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { getAuxArmType, parseWarDexAux } from './aux.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
@@ -29,8 +30,7 @@ function onCalcStat(
if (statName.startsWith('dex')) {
const myDex = getMetaNumber(context.general.meta, `dex${cavalryType}`);
const isAttacker = (aux as any)?.isAttacker;
const opposeType = (aux as any)?.opposeType;
const { isAttacker, opposeType } = parseWarDexAux(aux);
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
return (value as number) + myDex;
@@ -53,10 +53,9 @@ export const traitModule: TraitModule = {
'[군사] 기병 계통 징·모병비 -10%<br>[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산',
onCalcDomestic: (_context, turnType, varType, value, aux) => {
if (turnType === '징병' || turnType === '모병') {
if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) {
if ((aux as any).armType === 3) {
return value * 0.9;
}
const armType = getAuxArmType(aux);
if (varType === 'cost' && armType === 3) {
return value * 0.9;
}
}
return value;
@@ -4,6 +4,11 @@ import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
type WarUnitWithGeneral = WarUnit & { getGeneral: () => { meta: Record<string, unknown> } };
const hasGeneral = (unit: WarUnit): unit is WarUnitWithGeneral =>
'getGeneral' in unit && typeof (unit as { getGeneral?: unknown }).getGeneral === 'function';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
context: WarActionContext,
@@ -17,8 +22,10 @@ function onCalcStat(
value: number | [number, number],
aux?: unknown
): number | [number, number] {
if (statName === 'warCriticalRatio' && (aux as any)?.isAttacker) {
return (value as number) + 0.1;
const isAttacker = typeof aux === 'object' && aux !== null && (aux as { isAttacker?: unknown }).isAttacker === true;
if (statName === 'warCriticalRatio' && isAttacker && typeof value === 'number') {
return value + 0.1;
}
return value;
}
@@ -36,8 +43,8 @@ export const traitModule: TraitModule = {
let defenceMultiplier = 0.98;
// Note: unit.getGeneral() is only available for WarUnitGeneral.
// In a real scenario, we should check if unit is WarUnitGeneral.
if ('getGeneral' in unit) {
const killnum = getMetaNumber((unit as any).getGeneral().meta, 'rank_killnum', 0);
if (hasGeneral(unit)) {
const killnum = getMetaNumber(unit.getGeneral().meta, 'rank_killnum', 0);
const logVal = Math.log2(Math.max(1, killnum / 5));
attackMultiplier += logVal / 20;
defenceMultiplier -= logVal / 50;
@@ -99,9 +99,9 @@ export const traitModule: TraitModule = {
getName: () => '반계',
getInfo: () =>
'[전투] 상대의 계략 성공 확률 -10%p, 상대의 계략을 40% 확률로 되돌림, 반목 성공시 대미지 추가(+60% → +150%)',
onCalcOpposeStat: (_context, statName, value: any, _aux) => {
if (statName === 'warMagicSuccessProb') {
return (value as number) - 0.1;
onCalcOpposeStat: (_context, statName, value, _aux) => {
if (statName === 'warMagicSuccessProb' && typeof value === 'number') {
return value - 0.1;
}
return value;
},
@@ -3,6 +3,7 @@ import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/type
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { getAuxArmType, parseWarDexAux } from './aux.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
@@ -29,8 +30,7 @@ function onCalcStat(
if (statName.startsWith('dex')) {
const myDex = getMetaNumber(context.general.meta, `dex${footmanType}`);
const isAttacker = (aux as any)?.isAttacker;
const opposeType = (aux as any)?.opposeType;
const { isAttacker, opposeType } = parseWarDexAux(aux);
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
return (value as number) + myDex;
@@ -53,12 +53,11 @@ export const traitModule: TraitModule = {
'[군사] 보병 계통 징·모병비 -10%<br>[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산',
onCalcDomestic: (_context, turnType, varType, value, aux) => {
if (turnType === '징병' || turnType === '모병') {
if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) {
// Note: In a real scenario, we should check if aux.armType is footman.
// Since we don't have easy access to config here, we might need to assume legacy ID 1 or similar.
if ((aux as any).armType === 1) {
return value * 0.9;
}
const armType = getAuxArmType(aux);
// Note: In a real scenario, we should check if aux.armType is footman.
// Since we don't have easy access to config here, we might need to assume legacy ID 1 or similar.
if (varType === 'cost' && armType === 1) {
return value * 0.9;
}
}
return value;
@@ -6,6 +6,11 @@ import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/trigge
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
type AtmosUnit = WarUnit & { addAtmos: (amount: number) => void };
const canAddAtmos = (unit: WarUnit): unit is AtmosUnit =>
'addAtmos' in unit && typeof (unit as { addAtmos?: unknown }).addAtmos === 'function';
class che_위압시도 extends BaseWarUnitTrigger {
constructor(unit: WarUnit) {
super(unit, 10100);
@@ -48,8 +53,8 @@ class che_위압발동 extends BaseWarUnitTrigger {
oppose.getLogger().pushGeneralBattleDetailLog('상대에게 <R>위압</>받았다!', LogFormat.PLAIN);
self.getLogger().pushGeneralBattleDetailLog('상대에게 <C>위압</>을 줬다!', LogFormat.PLAIN);
oppose.setWarPowerMultiply(0);
if ('addAtmos' in oppose) {
(oppose as any).addAtmos(-5);
if (canAddAtmos(oppose)) {
oppose.addAtmos(-5);
}
return true;
@@ -6,6 +6,11 @@ import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/trigge
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
type AtmosUnit = WarUnit & { addAtmos: (amount: number) => void };
const canAddAtmos = (unit: WarUnit): unit is AtmosUnit =>
'addAtmos' in unit && typeof (unit as { addAtmos?: unknown }).addAtmos === 'function';
class che_저격시도 extends BaseWarUnitTrigger {
private readonly ratio: number;
private readonly woundMin: number;
@@ -86,8 +91,8 @@ class che_저격발동 extends BaseWarUnitTrigger {
self.getLogger().pushGeneralBattleDetailLog('성벽 수비대장을 <C>저격</>했다!', LogFormat.PLAIN);
}
if ('addAtmos' in self) {
(self as any).addAtmos(selfEnv['addAtmos'] as number);
if (canAddAtmos(self)) {
self.addAtmos(selfEnv['addAtmos'] as number);
}
return true;
+22 -20
View File
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
import type { City, General, Nation } from '../src/domain/entities.js';
import { resolveGeneralAction } from '../src/actions/engine.js';
import { ActionDefinition } from '../src/actions/turn/general/che_출병.js';
import type { DispatchResolveContext } from '../src/actions/turn/general/che_출병.js';
import type { TurnSchedule } from '../src/turn/calendar.js';
import type { WarAftermathConfig, WarEngineConfig } from '../src/war/types.js';
import type { UnitSetDefinition } from '../src/world/types.js';
@@ -184,28 +185,29 @@ describe('che_출병', () => {
const defender = buildGeneral(2, defenderNation.id, defenderCity.id);
const definition = new ActionDefinition();
const context: Omit<DispatchResolveContext, 'addLog'> = {
general: attacker,
city: attackerCity,
nation: attackerNation,
rng,
destCity: defenderCity,
destNation: defenderNation,
cities: [attackerCity, defenderCity],
nations: [attackerNation, defenderNation],
generals: [attacker, defender],
unitSet,
time: {
year: 200,
month: 1,
startYear: 180,
},
seedBase: 'test-seed',
warConfig,
aftermathConfig,
};
const resolution = resolveGeneralAction(
definition,
{
general: attacker,
city: attackerCity,
nation: attackerNation,
rng,
destCity: defenderCity,
destNation: defenderNation,
cities: [attackerCity, defenderCity],
nations: [attackerNation, defenderNation],
generals: [attacker, defender],
unitSet,
time: {
year: 200,
month: 1,
startYear: 180,
},
seedBase: 'test-seed',
warConfig,
aftermathConfig,
} as any,
context,
{
now: new Date('2000-01-01T00:00:00Z'),
schedule,