feat: 타입 정의 개선 및 any 사용 제거
This commit is contained in:
@@ -14,6 +14,18 @@ const buildDb = () => {
|
|||||||
const generalTurns = new Map<number, GeneralTurnRow[]>();
|
const generalTurns = new Map<number, GeneralTurnRow[]>();
|
||||||
const nationTurns = new Map<string, NationTurnRow[]>();
|
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 = {
|
const db = {
|
||||||
worldState: {
|
worldState: {
|
||||||
findFirst: async () => null,
|
findFirst: async () => null,
|
||||||
@@ -28,13 +40,18 @@ const buildDb = () => {
|
|||||||
findUnique: async () => null,
|
findUnique: async () => null,
|
||||||
},
|
},
|
||||||
generalTurn: {
|
generalTurn: {
|
||||||
findMany: async ({ where }: any) => generalTurns.get(where.generalId) ?? [],
|
findMany: async (args?: GeneralTurnFindManyArgs) => {
|
||||||
deleteMany: async ({ where }: any) => {
|
const generalId = typeof args?.where?.generalId === 'number' ? args.where.generalId : undefined;
|
||||||
generalTurns.delete(where.generalId);
|
return generalId !== undefined ? (generalTurns.get(generalId) ?? []) : [];
|
||||||
|
},
|
||||||
|
deleteMany: async ({ where }: GeneralTurnDeleteManyArgs) => {
|
||||||
|
if (typeof where.generalId === 'number') {
|
||||||
|
generalTurns.delete(where.generalId);
|
||||||
|
}
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
createMany: async ({ data }: any) => {
|
createMany: async ({ data }: GeneralTurnCreateManyArgs) => {
|
||||||
const rows = data.map((row: any, index: number) => ({
|
const rows = data.map((row: GeneralTurnCreateManyRow, index: number) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
generalId: row.generalId,
|
generalId: row.generalId,
|
||||||
turnIdx: row.turnIdx,
|
turnIdx: row.turnIdx,
|
||||||
@@ -49,13 +66,23 @@ const buildDb = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
nationTurn: {
|
nationTurn: {
|
||||||
findMany: async ({ where }: any) => nationTurns.get(`${where.nationId}:${where.officerLevel}`) ?? [],
|
findMany: async (args?: NationTurnFindManyArgs) => {
|
||||||
deleteMany: async ({ where }: any) => {
|
const nationId = typeof args?.where?.nationId === 'number' ? args.where.nationId : undefined;
|
||||||
nationTurns.delete(`${where.nationId}:${where.officerLevel}`);
|
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 {};
|
return {};
|
||||||
},
|
},
|
||||||
createMany: async ({ data }: any) => {
|
createMany: async ({ data }: NationTurnCreateManyArgs) => {
|
||||||
const rows = data.map((row: any, index: number) => ({
|
const rows = data.map((row: NationTurnCreateManyRow, index: number) => ({
|
||||||
id: index + 1,
|
id: index + 1,
|
||||||
nationId: row.nationId,
|
nationId: row.nationId,
|
||||||
officerLevel: row.officerLevel,
|
officerLevel: row.officerLevel,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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 { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.js';
|
||||||
import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js';
|
import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js';
|
||||||
import { appRouter } from '../src/router.js';
|
import { appRouter } from '../src/router.js';
|
||||||
@@ -43,12 +44,12 @@ const buildContext = (options?: {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
db: db as any,
|
db: db as unknown as DatabaseClient,
|
||||||
turnDaemon: transport,
|
turnDaemon: transport,
|
||||||
battleSim,
|
battleSim,
|
||||||
profile,
|
profile,
|
||||||
auth: null,
|
auth: null,
|
||||||
redis: {} as any,
|
redis: {} as unknown as RedisConnector['client'],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import { InMemoryGatewaySessionService } from '../src/auth/inMemorySessionService.js';
|
import { InMemoryGatewaySessionService } from '../src/auth/inMemorySessionService.js';
|
||||||
import { createInMemoryUserRepository } from '../src/auth/inMemoryUserRepository.js';
|
import { createInMemoryUserRepository } from '../src/auth/inMemoryUserRepository.js';
|
||||||
import { InMemoryOAuthSessionStore } from '../src/auth/oauthSessionStore.js';
|
import { InMemoryOAuthSessionStore } from '../src/auth/oauthSessionStore.js';
|
||||||
|
import type { KakaoOAuthClient } from '../src/auth/kakaoClient.js';
|
||||||
import { createGatewayApiContext } from '../src/context.js';
|
import { createGatewayApiContext } from '../src/context.js';
|
||||||
import { InMemoryProfileStatusService } from '../src/lobby/profileStatusService.js';
|
import { InMemoryProfileStatusService } from '../src/lobby/profileStatusService.js';
|
||||||
import { appRouter } from '../src/router.js';
|
import { appRouter } from '../src/router.js';
|
||||||
|
import type { GatewayPrismaClient } from '@sammo-ts/infra';
|
||||||
|
|
||||||
const buildCaller = () => {
|
const buildCaller = () => {
|
||||||
const users = createInMemoryUserRepository();
|
const users = createInMemoryUserRepository();
|
||||||
@@ -76,14 +78,14 @@ const buildCaller = () => {
|
|||||||
flushPublisher,
|
flushPublisher,
|
||||||
gameTokenSecret: 'test-secret',
|
gameTokenSecret: 'test-secret',
|
||||||
gameSessionTtlSeconds: 600,
|
gameSessionTtlSeconds: 600,
|
||||||
kakaoClient: kakaoClient as any,
|
kakaoClient: kakaoClient as unknown as KakaoOAuthClient,
|
||||||
oauthSessions,
|
oauthSessions,
|
||||||
publicBaseUrl: 'http://localhost',
|
publicBaseUrl: 'http://localhost',
|
||||||
profiles,
|
profiles,
|
||||||
orchestrator,
|
orchestrator,
|
||||||
profileStatus,
|
profileStatus,
|
||||||
requestHeaders: {},
|
requestHeaders: {},
|
||||||
prisma: {} as any,
|
prisma: {} as unknown as GatewayPrismaClient,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return { caller, oauthSessions };
|
return { caller, oauthSessions };
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
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 DefaultLayout from '../layouts/DefaultLayout.vue';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
import { createGameTrpc } from '../utils/gameTrpc';
|
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 router = useRouter();
|
||||||
const me = ref<any>(null);
|
const me = ref<MeOutput>(null);
|
||||||
const notice = ref('');
|
const notice = ref('');
|
||||||
const profiles = ref<any[]>([]);
|
const profiles = ref<LobbyProfile[]>([]);
|
||||||
const profileDetails = ref<Record<string, any>>({});
|
const profileDetails = ref<Record<string, LobbyInfo | undefined>>({});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export interface TurnCommandSpecBase<TKey extends string = string> {
|
|||||||
|
|
||||||
export interface TurnCommandModule<TSpec extends TurnCommandSpecBase = TurnCommandSpecBase> {
|
export interface TurnCommandModule<TSpec extends TurnCommandSpecBase = TurnCommandSpecBase> {
|
||||||
commandSpec: TSpec;
|
commandSpec: TSpec;
|
||||||
ActionDefinition: new (...args: any[]) => GeneralActionDefinition;
|
ActionDefinition: new (...args: unknown[]) => GeneralActionDefinition;
|
||||||
ActionResolver?: new (...args: any[]) => GeneralActionResolver;
|
ActionResolver?: new (...args: unknown[]) => GeneralActionResolver;
|
||||||
CommandResolver?: new (...args: any[]) => any;
|
CommandResolver?: new (...args: unknown[]) => unknown;
|
||||||
actionContextBuilder?: ActionContextBuilder;
|
actionContextBuilder?: ActionContextBuilder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export class ActionDefinition<
|
|||||||
if (typeof raw !== 'object' || raw === null) {
|
if (typeof raw !== 'object' || raw === null) {
|
||||||
return 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') {
|
if (typeof buyRice !== 'boolean' || typeof amount !== 'number') {
|
||||||
return null;
|
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',
|
kind: 'war',
|
||||||
getName: () => '견고',
|
getName: () => '견고',
|
||||||
getInfo: () => '[전투] 상대 필살 확률 -20%p, 상대 계략 시도시 성공 확률 -10%p, 부상 없음, 아군 피해 -10%',
|
getInfo: () => '[전투] 상대 필살 확률 -20%p, 상대 계략 시도시 성공 확률 -10%p, 부상 없음, 아군 피해 -10%',
|
||||||
onCalcOpposeStat: (_context, statName, value: any, _aux) => {
|
onCalcOpposeStat: (_context, statName, value, _aux) => {
|
||||||
if (statName === 'warMagicSuccessProb') {
|
if (statName === 'warMagicSuccessProb' && typeof value === 'number') {
|
||||||
return (value as number) - 0.1;
|
return value - 0.1;
|
||||||
}
|
}
|
||||||
if (statName === 'warCriticalRatio') {
|
if (statName === 'warCriticalRatio' && typeof value === 'number') {
|
||||||
return (value as number) - 0.2;
|
return value - 0.2;
|
||||||
}
|
}
|
||||||
return value;
|
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 type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
|
||||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
|
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
|
||||||
import { WarUnitCity } from '@sammo-ts/logic/war/units.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(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||||
function onCalcStat(
|
function onCalcStat(
|
||||||
@@ -30,8 +31,7 @@ function onCalcStat(
|
|||||||
|
|
||||||
if (statName.startsWith('dex')) {
|
if (statName.startsWith('dex')) {
|
||||||
const myDex = getMetaNumber(context.general.meta, `dex${siegeType}`);
|
const myDex = getMetaNumber(context.general.meta, `dex${siegeType}`);
|
||||||
const isAttacker = (aux as any)?.isAttacker;
|
const { isAttacker, opposeType } = parseWarDexAux(aux);
|
||||||
const opposeType = (aux as any)?.opposeType;
|
|
||||||
|
|
||||||
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
||||||
return (value as number) + myDex;
|
return (value as number) + myDex;
|
||||||
@@ -54,10 +54,9 @@ export const traitModule: TraitModule = {
|
|||||||
'[군사] 차병 계통 징·모병비 -10%<br>[전투] 성벽 공격 시 대미지 +100%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산',
|
'[군사] 차병 계통 징·모병비 -10%<br>[전투] 성벽 공격 시 대미지 +100%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산',
|
||||||
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
||||||
if (turnType === '징병' || turnType === '모병') {
|
if (turnType === '징병' || turnType === '모병') {
|
||||||
if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) {
|
const armType = getAuxArmType(aux);
|
||||||
if ((aux as any).armType === 4) {
|
if (varType === 'cost' && armType === 4) {
|
||||||
return value * 0.9;
|
return value * 0.9;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value;
|
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 { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||||
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
|
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
|
||||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.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(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||||
function onCalcStat(
|
function onCalcStat(
|
||||||
@@ -33,8 +34,7 @@ function onCalcStat(
|
|||||||
|
|
||||||
if (statName.startsWith('dex')) {
|
if (statName.startsWith('dex')) {
|
||||||
const myDex = getMetaNumber(context.general.meta, `dex${archerType}`);
|
const myDex = getMetaNumber(context.general.meta, `dex${archerType}`);
|
||||||
const isAttacker = (aux as any)?.isAttacker;
|
const { isAttacker, opposeType } = parseWarDexAux(aux);
|
||||||
const opposeType = (aux as any)?.opposeType;
|
|
||||||
|
|
||||||
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
||||||
return (value as number) + myDex;
|
return (value as number) + myDex;
|
||||||
@@ -57,12 +57,11 @@ export const traitModule: TraitModule = {
|
|||||||
'[군사] 궁병 계통 징·모병비 -10%<br>[전투] 회피 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산',
|
'[군사] 궁병 계통 징·모병비 -10%<br>[전투] 회피 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산',
|
||||||
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
||||||
if (turnType === '징병' || turnType === '모병') {
|
if (turnType === '징병' || turnType === '모병') {
|
||||||
if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) {
|
const armType = getAuxArmType(aux);
|
||||||
// Note: In a real scenario, we should check if aux.armType is archer.
|
// 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.
|
// 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) {
|
if (varType === 'cost' && armType === 2) {
|
||||||
return value * 0.9;
|
return value * 0.9;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value;
|
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 { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||||
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
|
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
|
||||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.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(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||||
function onCalcStat(
|
function onCalcStat(
|
||||||
@@ -29,8 +30,7 @@ function onCalcStat(
|
|||||||
|
|
||||||
if (statName.startsWith('dex')) {
|
if (statName.startsWith('dex')) {
|
||||||
const myDex = getMetaNumber(context.general.meta, `dex${cavalryType}`);
|
const myDex = getMetaNumber(context.general.meta, `dex${cavalryType}`);
|
||||||
const isAttacker = (aux as any)?.isAttacker;
|
const { isAttacker, opposeType } = parseWarDexAux(aux);
|
||||||
const opposeType = (aux as any)?.opposeType;
|
|
||||||
|
|
||||||
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
||||||
return (value as number) + myDex;
|
return (value as number) + myDex;
|
||||||
@@ -53,10 +53,9 @@ export const traitModule: TraitModule = {
|
|||||||
'[군사] 기병 계통 징·모병비 -10%<br>[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산',
|
'[군사] 기병 계통 징·모병비 -10%<br>[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산',
|
||||||
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
||||||
if (turnType === '징병' || turnType === '모병') {
|
if (turnType === '징병' || turnType === '모병') {
|
||||||
if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) {
|
const armType = getAuxArmType(aux);
|
||||||
if ((aux as any).armType === 3) {
|
if (varType === 'cost' && armType === 3) {
|
||||||
return value * 0.9;
|
return value * 0.9;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value;
|
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 type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
|
||||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.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: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||||
function onCalcStat(
|
function onCalcStat(
|
||||||
context: WarActionContext,
|
context: WarActionContext,
|
||||||
@@ -17,8 +22,10 @@ function onCalcStat(
|
|||||||
value: number | [number, number],
|
value: number | [number, number],
|
||||||
aux?: unknown
|
aux?: unknown
|
||||||
): number | [number, number] {
|
): number | [number, number] {
|
||||||
if (statName === 'warCriticalRatio' && (aux as any)?.isAttacker) {
|
const isAttacker = typeof aux === 'object' && aux !== null && (aux as { isAttacker?: unknown }).isAttacker === true;
|
||||||
return (value as number) + 0.1;
|
|
||||||
|
if (statName === 'warCriticalRatio' && isAttacker && typeof value === 'number') {
|
||||||
|
return value + 0.1;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -36,8 +43,8 @@ export const traitModule: TraitModule = {
|
|||||||
let defenceMultiplier = 0.98;
|
let defenceMultiplier = 0.98;
|
||||||
// Note: unit.getGeneral() is only available for WarUnitGeneral.
|
// Note: unit.getGeneral() is only available for WarUnitGeneral.
|
||||||
// In a real scenario, we should check if unit is WarUnitGeneral.
|
// In a real scenario, we should check if unit is WarUnitGeneral.
|
||||||
if ('getGeneral' in unit) {
|
if (hasGeneral(unit)) {
|
||||||
const killnum = getMetaNumber((unit as any).getGeneral().meta, 'rank_killnum', 0);
|
const killnum = getMetaNumber(unit.getGeneral().meta, 'rank_killnum', 0);
|
||||||
const logVal = Math.log2(Math.max(1, killnum / 5));
|
const logVal = Math.log2(Math.max(1, killnum / 5));
|
||||||
attackMultiplier += logVal / 20;
|
attackMultiplier += logVal / 20;
|
||||||
defenceMultiplier -= logVal / 50;
|
defenceMultiplier -= logVal / 50;
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ export const traitModule: TraitModule = {
|
|||||||
getName: () => '반계',
|
getName: () => '반계',
|
||||||
getInfo: () =>
|
getInfo: () =>
|
||||||
'[전투] 상대의 계략 성공 확률 -10%p, 상대의 계략을 40% 확률로 되돌림, 반목 성공시 대미지 추가(+60% → +150%)',
|
'[전투] 상대의 계략 성공 확률 -10%p, 상대의 계략을 40% 확률로 되돌림, 반목 성공시 대미지 추가(+60% → +150%)',
|
||||||
onCalcOpposeStat: (_context, statName, value: any, _aux) => {
|
onCalcOpposeStat: (_context, statName, value, _aux) => {
|
||||||
if (statName === 'warMagicSuccessProb') {
|
if (statName === 'warMagicSuccessProb' && typeof value === 'number') {
|
||||||
return (value as number) - 0.1;
|
return value - 0.1;
|
||||||
}
|
}
|
||||||
return value;
|
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 { WarActionContext } from '@sammo-ts/logic/war/actions.js';
|
||||||
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
|
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
|
||||||
import { getMetaNumber } from '@sammo-ts/logic/war/utils.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(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
|
||||||
function onCalcStat(
|
function onCalcStat(
|
||||||
@@ -29,8 +30,7 @@ function onCalcStat(
|
|||||||
|
|
||||||
if (statName.startsWith('dex')) {
|
if (statName.startsWith('dex')) {
|
||||||
const myDex = getMetaNumber(context.general.meta, `dex${footmanType}`);
|
const myDex = getMetaNumber(context.general.meta, `dex${footmanType}`);
|
||||||
const isAttacker = (aux as any)?.isAttacker;
|
const { isAttacker, opposeType } = parseWarDexAux(aux);
|
||||||
const opposeType = (aux as any)?.opposeType;
|
|
||||||
|
|
||||||
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
|
||||||
return (value as number) + myDex;
|
return (value as number) + myDex;
|
||||||
@@ -53,12 +53,11 @@ export const traitModule: TraitModule = {
|
|||||||
'[군사] 보병 계통 징·모병비 -10%<br>[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산',
|
'[군사] 보병 계통 징·모병비 -10%<br>[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산',
|
||||||
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
onCalcDomestic: (_context, turnType, varType, value, aux) => {
|
||||||
if (turnType === '징병' || turnType === '모병') {
|
if (turnType === '징병' || turnType === '모병') {
|
||||||
if (varType === 'cost' && aux && typeof aux === 'object' && 'armType' in aux) {
|
const armType = getAuxArmType(aux);
|
||||||
// Note: In a real scenario, we should check if aux.armType is footman.
|
// 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.
|
// 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) {
|
if (varType === 'cost' && armType === 1) {
|
||||||
return value * 0.9;
|
return value * 0.9;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value;
|
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 type { WarUnit } from '@sammo-ts/logic/war/units.js';
|
||||||
import { LogFormat } from '@sammo-ts/logic/logging/types.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 {
|
class che_위압시도 extends BaseWarUnitTrigger {
|
||||||
constructor(unit: WarUnit) {
|
constructor(unit: WarUnit) {
|
||||||
super(unit, 10100);
|
super(unit, 10100);
|
||||||
@@ -48,8 +53,8 @@ class che_위압발동 extends BaseWarUnitTrigger {
|
|||||||
oppose.getLogger().pushGeneralBattleDetailLog('상대에게 <R>위압</>받았다!', LogFormat.PLAIN);
|
oppose.getLogger().pushGeneralBattleDetailLog('상대에게 <R>위압</>받았다!', LogFormat.PLAIN);
|
||||||
self.getLogger().pushGeneralBattleDetailLog('상대에게 <C>위압</>을 줬다!', LogFormat.PLAIN);
|
self.getLogger().pushGeneralBattleDetailLog('상대에게 <C>위압</>을 줬다!', LogFormat.PLAIN);
|
||||||
oppose.setWarPowerMultiply(0);
|
oppose.setWarPowerMultiply(0);
|
||||||
if ('addAtmos' in oppose) {
|
if (canAddAtmos(oppose)) {
|
||||||
(oppose as any).addAtmos(-5);
|
oppose.addAtmos(-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
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 type { WarUnit } from '@sammo-ts/logic/war/units.js';
|
||||||
import { LogFormat } from '@sammo-ts/logic/logging/types.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 {
|
class che_저격시도 extends BaseWarUnitTrigger {
|
||||||
private readonly ratio: number;
|
private readonly ratio: number;
|
||||||
private readonly woundMin: number;
|
private readonly woundMin: number;
|
||||||
@@ -86,8 +91,8 @@ class che_저격발동 extends BaseWarUnitTrigger {
|
|||||||
self.getLogger().pushGeneralBattleDetailLog('성벽 수비대장을 <C>저격</>했다!', LogFormat.PLAIN);
|
self.getLogger().pushGeneralBattleDetailLog('성벽 수비대장을 <C>저격</>했다!', LogFormat.PLAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('addAtmos' in self) {
|
if (canAddAtmos(self)) {
|
||||||
(self as any).addAtmos(selfEnv['addAtmos'] as number);
|
self.addAtmos(selfEnv['addAtmos'] as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import type { City, General, Nation } from '../src/domain/entities.js';
|
import type { City, General, Nation } from '../src/domain/entities.js';
|
||||||
import { resolveGeneralAction } from '../src/actions/engine.js';
|
import { resolveGeneralAction } from '../src/actions/engine.js';
|
||||||
import { ActionDefinition } from '../src/actions/turn/general/che_출병.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 { TurnSchedule } from '../src/turn/calendar.js';
|
||||||
import type { WarAftermathConfig, WarEngineConfig } from '../src/war/types.js';
|
import type { WarAftermathConfig, WarEngineConfig } from '../src/war/types.js';
|
||||||
import type { UnitSetDefinition } from '../src/world/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 defender = buildGeneral(2, defenderNation.id, defenderCity.id);
|
||||||
|
|
||||||
const definition = new ActionDefinition();
|
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(
|
const resolution = resolveGeneralAction(
|
||||||
definition,
|
definition,
|
||||||
{
|
context,
|
||||||
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,
|
|
||||||
{
|
{
|
||||||
now: new Date('2000-01-01T00:00:00Z'),
|
now: new Date('2000-01-01T00:00:00Z'),
|
||||||
schedule,
|
schedule,
|
||||||
|
|||||||
Reference in New Issue
Block a user