merge: restore lint and test baseline
This commit is contained in:
@@ -55,7 +55,7 @@ export const runBattleSimWorker = async (options: BattleSimWorkerOptions = {}):
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let job: BattleSimJob | null = null;
|
let job: BattleSimJob;
|
||||||
try {
|
try {
|
||||||
job = JSON.parse(raw) as BattleSimJob;
|
job = JSON.parse(raw) as BattleSimJob;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import { TRPCError } from '@trpc/server';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { asRecord } from '@sammo-ts/common';
|
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 { authedProcedure, router } from '../../trpc.js';
|
||||||
import { getMyGeneral } from '../shared/general.js';
|
import { getMyGeneral } from '../shared/general.js';
|
||||||
import { assertNationAccess, resolveNationPermission } from '../nation/shared.js';
|
import { assertNationAccess, resolveNationPermission } from '../nation/shared.js';
|
||||||
|
|
||||||
const zLetterState = z.enum(['PROPOSED', 'ACTIVATED', 'CANCELLED', 'REPLACED']);
|
|
||||||
|
|
||||||
const resolvePermissionLevel = async (ctx: Parameters<typeof getMyGeneral>[0], nationId: number) => {
|
const resolvePermissionLevel = async (ctx: Parameters<typeof getMyGeneral>[0], nationId: number) => {
|
||||||
const nation = await ctx.db.nation.findUnique({
|
const nation = await ctx.db.nation.findUnique({
|
||||||
where: { id: nationId },
|
where: { id: nationId },
|
||||||
@@ -22,7 +20,7 @@ const resolvePermissionLevel = async (ctx: Parameters<typeof getMyGeneral>[0], n
|
|||||||
return resolveNationPermission(general, nation.meta, true);
|
return resolveNationPermission(general, nation.meta, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapLetterState = (state: string): z.infer<typeof zLetterState> => {
|
const mapLetterState = (state: string): 'PROPOSED' | 'ACTIVATED' | 'CANCELLED' | 'REPLACED' => {
|
||||||
if (state === 'ACTIVATED') return 'ACTIVATED';
|
if (state === 'ACTIVATED') return 'ACTIVATED';
|
||||||
if (state === 'CANCELLED') return 'CANCELLED';
|
if (state === 'CANCELLED') return 'CANCELLED';
|
||||||
if (state === 'REPLACED') return 'REPLACED';
|
if (state === 'REPLACED') return 'REPLACED';
|
||||||
@@ -153,7 +151,10 @@ export const diplomacyRouter = router({
|
|||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if (newer) {
|
if (newer) {
|
||||||
throw new TRPCError({ code: 'BAD_REQUEST', message: '해당 문서에 대한 새로운 문서가 이미 있습니다.' });
|
throw new TRPCError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: '해당 문서에 대한 새로운 문서가 이미 있습니다.',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevLetter.state === 'PROPOSED') {
|
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({
|
const nations = await ctx.db.nation.findMany({
|
||||||
|
|||||||
@@ -711,7 +711,7 @@ export class GeneralAI {
|
|||||||
const leadership = this.general.stats.leadership;
|
const leadership = this.general.stats.leadership;
|
||||||
const strength = Math.max(this.general.stats.strength, 1);
|
const strength = Math.max(this.general.stats.strength, 1);
|
||||||
const intel = Math.max(this.general.stats.intelligence, 1);
|
const intel = Math.max(this.general.stats.intelligence, 1);
|
||||||
let genType = 0;
|
let genType: number;
|
||||||
|
|
||||||
if (strength >= intel) {
|
if (strength >= intel) {
|
||||||
genType = t무장;
|
genType = t무장;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const do부대전방발령 = (ai: GeneralAI) => {
|
|||||||
const force = ai.nationPolicy.combatForce[leader.id];
|
const force = ai.nationPolicy.combatForce[leader.id];
|
||||||
let [fromCityId, toCityId] = force;
|
let [fromCityId, toCityId] = force;
|
||||||
|
|
||||||
let targetCityId: number | null = null;
|
let targetCityId: number | null;
|
||||||
if (!ai.warRoute || !ai.warRoute[fromCityId] || ai.warRoute[fromCityId][toCityId] === undefined) {
|
if (!ai.warRoute || !ai.warRoute[fromCityId] || ai.warRoute[fromCityId][toCityId] === undefined) {
|
||||||
targetCityId = pickRandomCityId(ai, ai.frontCities);
|
targetCityId = pickRandomCityId(ai, ai.frontCities);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const applyIncomeOutcome = (
|
|||||||
originOutcome: number
|
originOutcome: number
|
||||||
): { next: number; ratio: number; realOutcome: number } => {
|
): { next: number; ratio: number; realOutcome: number } => {
|
||||||
let next = current + income;
|
let next = current + income;
|
||||||
let realOutcome = 0;
|
let realOutcome: number;
|
||||||
if (next < baseResource) {
|
if (next < baseResource) {
|
||||||
realOutcome = 0;
|
realOutcome = 0;
|
||||||
next = baseResource;
|
next = baseResource;
|
||||||
@@ -139,14 +139,11 @@ const processIncomeForNation = (
|
|||||||
const trait = traitMap.get(nation.typeCode) ?? null;
|
const trait = traitMap.get(nation.typeCode) ?? null;
|
||||||
const incomeContext = buildNationIncomeContext(nation, trait);
|
const incomeContext = buildNationIncomeContext(nation, trait);
|
||||||
|
|
||||||
let income = 0;
|
const income =
|
||||||
if (type === 'gold') {
|
type === 'gold'
|
||||||
income = getGoldIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level);
|
? getGoldIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level)
|
||||||
} else {
|
: getRiceIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level) +
|
||||||
income =
|
getWallIncome(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 incomeValue = roundResource(income);
|
||||||
const originOutcome = getOutcome(100, nationGenerals);
|
const originOutcome = getOutcome(100, nationGenerals);
|
||||||
|
|||||||
@@ -167,7 +167,8 @@ export const createUnificationHandler = (options: {
|
|||||||
sabotage,
|
sabotage,
|
||||||
dex,
|
dex,
|
||||||
unifier,
|
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 meta = asRecord(state.meta);
|
||||||
|
|
||||||
const serverId =
|
const serverId =
|
||||||
typeof meta.serverId === 'string' && meta.serverId.trim()
|
typeof meta.serverId === 'string' && meta.serverId.trim() ? meta.serverId.trim() : options.profileName;
|
||||||
? meta.serverId.trim()
|
|
||||||
: options.profileName;
|
|
||||||
const season = readMetaNumberOrNull(meta, 'season') ?? 1;
|
const season = readMetaNumberOrNull(meta, 'season') ?? 1;
|
||||||
const scenario = readMetaNumberOrNull(meta, 'scenarioId') ?? 0;
|
const scenario = readMetaNumberOrNull(meta, 'scenarioId') ?? 0;
|
||||||
const scenarioName =
|
const scenarioName =
|
||||||
typeof asRecord(meta.scenarioMeta).title === 'string'
|
typeof asRecord(meta.scenarioMeta).title === 'string' ? String(asRecord(meta.scenarioMeta).title) : '';
|
||||||
? String(asRecord(meta.scenarioMeta).title)
|
|
||||||
: '';
|
|
||||||
const startTime = typeof meta.starttime === 'string' ? meta.starttime : null;
|
const startTime = typeof meta.starttime === 'string' ? meta.starttime : null;
|
||||||
const unitedTime = new Date().toISOString();
|
const unitedTime = new Date().toISOString();
|
||||||
|
|
||||||
@@ -307,14 +304,16 @@ export const createUnificationHandler = (options: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (const [typeName, valueType] of hallTypes) {
|
for (const [typeName, valueType] of hallTypes) {
|
||||||
let value = 0;
|
const value =
|
||||||
if (valueType === 'natural') {
|
valueType === 'natural'
|
||||||
value = typeName === 'experience' ? general.experience : typeName === 'dedication' ? general.dedication : ranks[typeName] ?? 0;
|
? typeName === 'experience'
|
||||||
} else if (valueType === 'rank') {
|
? general.experience
|
||||||
value = ranks[typeName] ?? 0;
|
: typeName === 'dedication'
|
||||||
} else {
|
? general.dedication
|
||||||
value = calcValues[typeName] ?? 0;
|
: (ranks[typeName] ?? 0)
|
||||||
}
|
: valueType === 'rank'
|
||||||
|
? (ranks[typeName] ?? 0)
|
||||||
|
: (calcValues[typeName] ?? 0);
|
||||||
|
|
||||||
if ((typeName === 'winrate' || typeName === 'killrate') && warnum < 10) {
|
if ((typeName === 'winrate' || typeName === 'killrate') && warnum < 10) {
|
||||||
continue;
|
continue;
|
||||||
@@ -391,9 +390,7 @@ export const createUnificationHandler = (options: {
|
|||||||
const meta = asRecord(state.meta);
|
const meta = asRecord(state.meta);
|
||||||
|
|
||||||
const serverId =
|
const serverId =
|
||||||
typeof meta.serverId === 'string' && meta.serverId.trim()
|
typeof meta.serverId === 'string' && meta.serverId.trim() ? meta.serverId.trim() : options.profileName;
|
||||||
? meta.serverId.trim()
|
|
||||||
: options.profileName;
|
|
||||||
const serverName =
|
const serverName =
|
||||||
typeof meta.serverName === 'string' && meta.serverName.trim()
|
typeof meta.serverName === 'string' && meta.serverName.trim()
|
||||||
? meta.serverName.trim()
|
? meta.serverName.trim()
|
||||||
@@ -653,30 +650,30 @@ export const createUnificationHandler = (options: {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
oldGeneralTargets.map((general) =>
|
oldGeneralTargets.map((general) =>
|
||||||
((snapshot) =>
|
((snapshot) =>
|
||||||
prisma.oldGeneral.upsert({
|
prisma.oldGeneral.upsert({
|
||||||
where: {
|
where: {
|
||||||
by_no: {
|
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,
|
serverId,
|
||||||
generalNo: general.id,
|
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,
|
...general,
|
||||||
turnTime: general.turnTime.toISOString(),
|
turnTime: general.turnTime.toISOString(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -315,8 +315,8 @@ async function handleTournamentMatchResult(
|
|||||||
const attackerG = getRankNumber(attacker, rankKey('g'));
|
const attackerG = getRankNumber(attacker, rankKey('g'));
|
||||||
const defenderG = getRankNumber(defender, rankKey('g'));
|
const defenderG = getRankNumber(defender, rankKey('g'));
|
||||||
|
|
||||||
let attackerGDelta = 0;
|
let attackerGDelta: number;
|
||||||
let defenderGDelta = 0;
|
let defenderGDelta: number;
|
||||||
let attackerW = 0;
|
let attackerW = 0;
|
||||||
let attackerD = 0;
|
let attackerD = 0;
|
||||||
let attackerL = 0;
|
let attackerL = 0;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
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';
|
import { DatabaseTurnDaemonCommandQueue } from '../src/lifecycle/databaseCommandQueue.js';
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ describe('레거시 사령부 턴 실행 호환성', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('MONTH action 전에 전략·외교 제한, 임시 세율, 첩보 기간을 갱신한다', () => {
|
it('MONTH action 전에 전략·외교 제한, 임시 세율, 첩보 기간을 갱신한다', async () => {
|
||||||
const updates: Array<{ id: number; patch: Record<string, unknown> }> = [];
|
const updates: Array<{ id: number; patch: Record<string, unknown> }> = [];
|
||||||
const nations = [
|
const nations = [
|
||||||
{
|
{
|
||||||
@@ -188,7 +188,7 @@ describe('레거시 사령부 턴 실행 호환성', () => {
|
|||||||
}) as never,
|
}) as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
handler.beforeMonthChanged?.({} as never);
|
await handler.beforeMonthChanged?.({} as never);
|
||||||
|
|
||||||
expect(updates).toEqual([
|
expect(updates).toEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -327,5 +327,5 @@ describe('NPC 기술 연구 장기 시뮬레이션', () => {
|
|||||||
// Nation awards can occur in the same tick and make the general's net
|
// 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
|
// gold delta smaller than the recruitment price. Exact cost scaling is
|
||||||
// covered by the unit-set/action contract tests rather than this smoke.
|
// covered by the unit-set/action contract tests rather than this smoke.
|
||||||
}, 60000);
|
}, 300_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -553,5 +553,5 @@ describe('NPC 건국/통일 장기 시뮬레이션', () => {
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}, 180000);
|
}, 360_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,5 +13,7 @@ export default defineConfig({
|
|||||||
environment: 'node',
|
environment: 'node',
|
||||||
globals: true,
|
globals: true,
|
||||||
include: ['test/**/*.test.ts'],
|
include: ['test/**/*.test.ts'],
|
||||||
|
maxWorkers: 4,
|
||||||
|
testTimeout: 10_000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { computed, ref } from 'vue';
|
|||||||
import type { BattleSimOptions, GeneralDraft } from '../../utils/battleSimulatorTypes';
|
import type { BattleSimOptions, GeneralDraft } from '../../utils/battleSimulatorTypes';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
general: GeneralDraft;
|
|
||||||
options: BattleSimOptions;
|
options: BattleSimOptions;
|
||||||
mode: 'attacker' | 'defender';
|
mode: 'attacker' | 'defender';
|
||||||
title: string;
|
title: string;
|
||||||
@@ -11,6 +10,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
const general = defineModel<GeneralDraft>('general', { required: true });
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'import'): void;
|
(event: 'import'): void;
|
||||||
|
|||||||
@@ -24,11 +24,10 @@ export const formatLog = (text?: string): string => {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let match: RegExpExecArray | null = null;
|
|
||||||
let lastIndex = 0;
|
let lastIndex = 0;
|
||||||
const result: string[] = [];
|
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 partAll = match[0];
|
||||||
const subPart = match[1];
|
const subPart = match[1];
|
||||||
const index = match.index;
|
const index = match.index;
|
||||||
@@ -40,9 +39,7 @@ export const formatLog = (text?: string): string => {
|
|||||||
if (subPart === '/') {
|
if (subPart === '/') {
|
||||||
result.push('</span>');
|
result.push('</span>');
|
||||||
} else if (subPart.length === 2) {
|
} else if (subPart.length === 2) {
|
||||||
result.push(
|
result.push(`<span style="${convertMap[subPart[0]] ?? ''}${convertMap2[subPart[1]] ?? ''}">`);
|
||||||
`<span style="${convertMap[subPart[0]] ?? ''}${convertMap2[subPart[1]] ?? ''}">`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
result.push(`<span style="${convertMap[subPart] ?? ''}">`);
|
result.push(`<span style="${convertMap[subPart] ?? ''}">`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1125,7 +1125,7 @@ const shouldShowUI = computed(() => !loading.value && !!options.value);
|
|||||||
|
|
||||||
<BattleGeneralCard
|
<BattleGeneralCard
|
||||||
v-if="attackerGeneral"
|
v-if="attackerGeneral"
|
||||||
:general="attackerGeneral!"
|
v-model:general="attackerGeneral"
|
||||||
:options="options!"
|
:options="options!"
|
||||||
mode="attacker"
|
mode="attacker"
|
||||||
title="출병자 설정"
|
title="출병자 설정"
|
||||||
@@ -1193,7 +1193,7 @@ const shouldShowUI = computed(() => !loading.value && !!options.value);
|
|||||||
<BattleGeneralCard
|
<BattleGeneralCard
|
||||||
v-for="(defender, index) in defenders"
|
v-for="(defender, index) in defenders"
|
||||||
:key="defender.id"
|
:key="defender.id"
|
||||||
:general="defender"
|
v-model:general="defenders[index]"
|
||||||
:options="options!"
|
:options="options!"
|
||||||
mode="defender"
|
mode="defender"
|
||||||
:title="`수비자 설정 ${index + 1}`"
|
:title="`수비자 설정 ${index + 1}`"
|
||||||
|
|||||||
@@ -189,9 +189,7 @@ const destroyLetter = async (letterId: number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevOptions = computed(() =>
|
const prevOptions = computed(() => data.value?.letters.filter((letter) => letter.state !== 'CANCELLED') ?? []);
|
||||||
data.value?.letters.filter((letter) => letter.state !== 'CANCELLED') ?? []
|
|
||||||
);
|
|
||||||
|
|
||||||
const formatDate = (value: string) => new Date(value).toLocaleString('ko-KR');
|
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';
|
editable.value && data.value?.myNationId === letter.src.nationId && letter.state === 'PROPOSED';
|
||||||
|
|
||||||
const canDestroy = (letter: DiplomacyLetter) =>
|
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';
|
const canRenew = (letter: DiplomacyLetter) => letter.state !== 'CANCELLED';
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadLetters();
|
void loadLetters();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -270,26 +270,84 @@ onBeforeUnmount(() => {
|
|||||||
<div class="editor-group">
|
<div class="editor-group">
|
||||||
<div class="editor-label">내용(국가 내 공개)</div>
|
<div class="editor-label">내용(국가 내 공개)</div>
|
||||||
<div class="editor-toolbar">
|
<div class="editor-toolbar">
|
||||||
<button type="button" @click="briefEditor?.chain().focus().toggleBold().run()" :class="{ active: briefEditor?.isActive('bold') }">굵게</button>
|
<button
|
||||||
<button type="button" @click="briefEditor?.chain().focus().toggleItalic().run()" :class="{ active: briefEditor?.isActive('italic') }">기울임</button>
|
type="button"
|
||||||
<button type="button" @click="briefEditor?.chain().focus().toggleUnderline().run()" :class="{ active: briefEditor?.isActive('underline') }">밑줄</button>
|
@click="briefEditor?.chain().focus().toggleBold().run()"
|
||||||
|
:class="{ active: briefEditor?.isActive('bold') }"
|
||||||
|
>
|
||||||
|
굵게
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="briefEditor?.chain().focus().toggleItalic().run()"
|
||||||
|
:class="{ active: briefEditor?.isActive('italic') }"
|
||||||
|
>
|
||||||
|
기울임
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="briefEditor?.chain().focus().toggleUnderline().run()"
|
||||||
|
:class="{ active: briefEditor?.isActive('underline') }"
|
||||||
|
>
|
||||||
|
밑줄
|
||||||
|
</button>
|
||||||
<button type="button" @click="addLink('brief')">링크</button>
|
<button type="button" @click="addLink('brief')">링크</button>
|
||||||
<button type="button" @click="briefEditor?.chain().focus().toggleBulletList().run()">목록</button>
|
<button type="button" @click="briefEditor?.chain().focus().toggleBulletList().run()">목록</button>
|
||||||
<button type="button" @click="briefEditor?.chain().focus().toggleOrderedList().run()">번호 목록</button>
|
<button type="button" @click="briefEditor?.chain().focus().toggleOrderedList().run()">
|
||||||
<button type="button" @click="uploadTarget = 'brief'; fileInputRef?.click()" :disabled="uploadBusy">이미지 업로드</button>
|
번호 목록
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="
|
||||||
|
uploadTarget = 'brief';
|
||||||
|
fileInputRef?.click();
|
||||||
|
"
|
||||||
|
:disabled="uploadBusy"
|
||||||
|
>
|
||||||
|
이미지 업로드
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<EditorContent v-if="briefEditor" :editor="briefEditor" />
|
<EditorContent v-if="briefEditor" :editor="briefEditor" />
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-group">
|
<div class="editor-group">
|
||||||
<div class="editor-label">내용(외교권자 전용)</div>
|
<div class="editor-label">내용(외교권자 전용)</div>
|
||||||
<div class="editor-toolbar">
|
<div class="editor-toolbar">
|
||||||
<button type="button" @click="detailEditor?.chain().focus().toggleBold().run()" :class="{ active: detailEditor?.isActive('bold') }">굵게</button>
|
<button
|
||||||
<button type="button" @click="detailEditor?.chain().focus().toggleItalic().run()" :class="{ active: detailEditor?.isActive('italic') }">기울임</button>
|
type="button"
|
||||||
<button type="button" @click="detailEditor?.chain().focus().toggleUnderline().run()" :class="{ active: detailEditor?.isActive('underline') }">밑줄</button>
|
@click="detailEditor?.chain().focus().toggleBold().run()"
|
||||||
|
:class="{ active: detailEditor?.isActive('bold') }"
|
||||||
|
>
|
||||||
|
굵게
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="detailEditor?.chain().focus().toggleItalic().run()"
|
||||||
|
:class="{ active: detailEditor?.isActive('italic') }"
|
||||||
|
>
|
||||||
|
기울임
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="detailEditor?.chain().focus().toggleUnderline().run()"
|
||||||
|
:class="{ active: detailEditor?.isActive('underline') }"
|
||||||
|
>
|
||||||
|
밑줄
|
||||||
|
</button>
|
||||||
<button type="button" @click="addLink('detail')">링크</button>
|
<button type="button" @click="addLink('detail')">링크</button>
|
||||||
<button type="button" @click="detailEditor?.chain().focus().toggleBulletList().run()">목록</button>
|
<button type="button" @click="detailEditor?.chain().focus().toggleBulletList().run()">목록</button>
|
||||||
<button type="button" @click="detailEditor?.chain().focus().toggleOrderedList().run()">번호 목록</button>
|
<button type="button" @click="detailEditor?.chain().focus().toggleOrderedList().run()">
|
||||||
<button type="button" @click="uploadTarget = 'detail'; fileInputRef?.click()" :disabled="uploadBusy">이미지 업로드</button>
|
번호 목록
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="
|
||||||
|
uploadTarget = 'detail';
|
||||||
|
fileInputRef?.click();
|
||||||
|
"
|
||||||
|
:disabled="uploadBusy"
|
||||||
|
>
|
||||||
|
이미지 업로드
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<EditorContent v-if="detailEditor" :editor="detailEditor" />
|
<EditorContent v-if="detailEditor" :editor="detailEditor" />
|
||||||
</div>
|
</div>
|
||||||
@@ -327,7 +385,10 @@ onBeforeUnmount(() => {
|
|||||||
</button>
|
</button>
|
||||||
<div v-if="historyOpen[letter.id]" class="history-panel">
|
<div v-if="historyOpen[letter.id]" class="history-panel">
|
||||||
<template v-if="getPrevLetter(letter)">
|
<template v-if="getPrevLetter(letter)">
|
||||||
<p>#{{ getPrevLetter(letter)?.id }} {{ getPrevLetter(letter)?.src.nationName }} ↔ {{ getPrevLetter(letter)?.dest.nationName }}</p>
|
<p>
|
||||||
|
#{{ getPrevLetter(letter)?.id }} {{ getPrevLetter(letter)?.src.nationName }} ↔
|
||||||
|
{{ getPrevLetter(letter)?.dest.nationName }}
|
||||||
|
</p>
|
||||||
<div class="letter-text" v-html="getPrevLetter(letter)?.brief" />
|
<div class="letter-text" v-html="getPrevLetter(letter)?.brief" />
|
||||||
</template>
|
</template>
|
||||||
<p v-else class="hint">이전 문서를 찾을 수 없습니다.</p>
|
<p v-else class="hint">이전 문서를 찾을 수 없습니다.</p>
|
||||||
@@ -335,11 +396,24 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="letter-actions">
|
<footer class="letter-actions">
|
||||||
<button v-if="canRespond(letter)" type="button" @click="respondLetter(letter.id, true)">승인</button>
|
<button v-if="canRespond(letter)" type="button" @click="respondLetter(letter.id, true)">
|
||||||
<button v-if="canRespond(letter)" type="button" @click="respondLetter(letter.id, false, '거부')">거부</button>
|
승인
|
||||||
|
</button>
|
||||||
|
<button v-if="canRespond(letter)" type="button" @click="respondLetter(letter.id, false, '거부')">
|
||||||
|
거부
|
||||||
|
</button>
|
||||||
<button v-if="canRollback(letter)" type="button" @click="rollbackLetter(letter.id)">회수</button>
|
<button v-if="canRollback(letter)" type="button" @click="rollbackLetter(letter.id)">회수</button>
|
||||||
<button v-if="canDestroy(letter)" type="button" @click="destroyLetter(letter.id)">파기</button>
|
<button v-if="canDestroy(letter)" type="button" @click="destroyLetter(letter.id)">파기</button>
|
||||||
<button v-if="canRenew(letter)" type="button" @click="selectedPrevId = letter.id; applyPrevLetter()">추가 문서 작성</button>
|
<button
|
||||||
|
v-if="canRenew(letter)"
|
||||||
|
type="button"
|
||||||
|
@click="
|
||||||
|
selectedPrevId = letter.id;
|
||||||
|
applyPrevLetter();
|
||||||
|
"
|
||||||
|
>
|
||||||
|
추가 문서 작성
|
||||||
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData();
|
void loadData();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -278,19 +278,17 @@ onBeforeUnmount(() => {
|
|||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>국가 방침</h2>
|
<h2>국가 방침</h2>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<button v-if="editable && !editingNationMsg" type="button" @click="startEditNationMsg">
|
<button v-if="editable && !editingNationMsg" type="button" @click="startEditNationMsg">수정</button>
|
||||||
수정
|
<button v-if="editable && editingNationMsg" type="button" @click="saveNationMsg">저장</button>
|
||||||
</button>
|
<button v-if="editable && editingNationMsg" type="button" @click="cancelEditNationMsg">취소</button>
|
||||||
<button v-if="editable && editingNationMsg" type="button" @click="saveNationMsg">
|
|
||||||
저장
|
|
||||||
</button>
|
|
||||||
<button v-if="editable && editingNationMsg" type="button" @click="cancelEditNationMsg">
|
|
||||||
취소
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editingNationMsg" class="editor-toolbar">
|
<div v-if="editingNationMsg" class="editor-toolbar">
|
||||||
<button type="button" @click="editor?.chain().focus().toggleBold().run()" :class="{ active: editor?.isActive('bold') }">
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="editor?.chain().focus().toggleBold().run()"
|
||||||
|
:class="{ active: editor?.isActive('bold') }"
|
||||||
|
>
|
||||||
굵게
|
굵게
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -323,21 +321,57 @@ onBeforeUnmount(() => {
|
|||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<h3>자금 예산</h3>
|
<h3>자금 예산</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<div><dt>현재</dt><dd>{{ data.gold.toLocaleString() }}</dd></div>
|
<div>
|
||||||
<div><dt>단기 수입</dt><dd>{{ data.income.gold.war.toLocaleString() }}</dd></div>
|
<dt>현재</dt>
|
||||||
<div><dt>세금</dt><dd>{{ Math.floor(incomeGoldCity).toLocaleString() }}</dd></div>
|
<dd>{{ data.gold.toLocaleString() }}</dd>
|
||||||
<div><dt>수입/지출</dt><dd>+{{ Math.floor(incomeGold).toLocaleString() }} / {{ Math.floor(-outcomeByBill).toLocaleString() }}</dd></div>
|
</div>
|
||||||
<div><dt>국고 예산</dt><dd>{{ Math.floor(data.gold + incomeGold - outcomeByBill).toLocaleString() }}</dd></div>
|
<div>
|
||||||
|
<dt>단기 수입</dt>
|
||||||
|
<dd>{{ data.income.gold.war.toLocaleString() }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>세금</dt>
|
||||||
|
<dd>{{ Math.floor(incomeGoldCity).toLocaleString() }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>수입/지출</dt>
|
||||||
|
<dd>
|
||||||
|
+{{ Math.floor(incomeGold).toLocaleString() }} /
|
||||||
|
{{ Math.floor(-outcomeByBill).toLocaleString() }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>국고 예산</dt>
|
||||||
|
<dd>{{ Math.floor(data.gold + incomeGold - outcomeByBill).toLocaleString() }}</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<h3>군량 예산</h3>
|
<h3>군량 예산</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<div><dt>현재</dt><dd>{{ data.rice.toLocaleString() }}</dd></div>
|
<div>
|
||||||
<div><dt>둔전 수입</dt><dd>{{ Math.floor(incomeRiceWall).toLocaleString() }}</dd></div>
|
<dt>현재</dt>
|
||||||
<div><dt>세금</dt><dd>{{ Math.floor(incomeRiceCity).toLocaleString() }}</dd></div>
|
<dd>{{ data.rice.toLocaleString() }}</dd>
|
||||||
<div><dt>수입/지출</dt><dd>+{{ Math.floor(incomeRice).toLocaleString() }} / {{ Math.floor(-outcomeByBill).toLocaleString() }}</dd></div>
|
</div>
|
||||||
<div><dt>국고 예산</dt><dd>{{ Math.floor(data.rice + incomeRice - outcomeByBill).toLocaleString() }}</dd></div>
|
<div>
|
||||||
|
<dt>둔전 수입</dt>
|
||||||
|
<dd>{{ Math.floor(incomeRiceWall).toLocaleString() }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>세금</dt>
|
||||||
|
<dd>{{ Math.floor(incomeRiceCity).toLocaleString() }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>수입/지출</dt>
|
||||||
|
<dd>
|
||||||
|
+{{ Math.floor(incomeRice).toLocaleString() }} /
|
||||||
|
{{ Math.floor(-outcomeByBill).toLocaleString() }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>국고 예산</dt>
|
||||||
|
<dd>{{ Math.floor(data.rice + incomeRice - outcomeByBill).toLocaleString() }}</dd>
|
||||||
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
@@ -359,7 +393,13 @@ onBeforeUnmount(() => {
|
|||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
<h3>기밀 권한</h3>
|
<h3>기밀 권한</h3>
|
||||||
<div class="input-row">
|
<div class="input-row">
|
||||||
<input v-model.number="policyDraft.secretLimit" type="number" min="1" max="99" :disabled="!editable" />
|
<input
|
||||||
|
v-model.number="policyDraft.secretLimit"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="99"
|
||||||
|
:disabled="!editable"
|
||||||
|
/>
|
||||||
<span>년</span>
|
<span>년</span>
|
||||||
<button type="button" @click="setSecretLimit" :disabled="!editable">변경</button>
|
<button type="button" @click="setSecretLimit" :disabled="!editable">변경</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,7 +416,9 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
전쟁 금지
|
전쟁 금지
|
||||||
</label>
|
</label>
|
||||||
<span class="hint">잔여 {{ data.warSettingCnt.remain }}회 (월 +{{ data.warSettingCnt.inc }}회)</span>
|
<span class="hint"
|
||||||
|
>잔여 {{ data.warSettingCnt.remain }}회 (월 +{{ data.warSettingCnt.inc }}회)</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-card">
|
<div class="panel-card">
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData();
|
void loadData();
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -168,7 +168,11 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="editing" class="editor-toolbar">
|
<div v-if="editing" class="editor-toolbar">
|
||||||
<button type="button" @click="editor?.chain().focus().toggleBold().run()" :class="{ active: editor?.isActive('bold') }">
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="editor?.chain().focus().toggleBold().run()"
|
||||||
|
:class="{ active: editor?.isActive('bold') }"
|
||||||
|
>
|
||||||
굵게
|
굵게
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -124,8 +124,11 @@ export const parsePercent = (value: string): number | null => {
|
|||||||
export type CompareOperator = '>' | '>=' | '==' | '<=' | '<' | '!=' | '===' | '!==';
|
export type CompareOperator = '>' | '>=' | '==' | '<=' | '<' | '!=' | '===' | '!==';
|
||||||
|
|
||||||
export const compareValues = (target: unknown, op: CompareOperator, source: unknown): boolean => {
|
export const compareValues = (target: unknown, op: CompareOperator, source: unknown): boolean => {
|
||||||
const lhs = target as any;
|
// The cast is type-only: JavaScript still applies its native relational
|
||||||
const rhs = source as any;
|
// coercion rules to the original runtime values, matching the legacy
|
||||||
|
// constraint evaluator without opting the whole comparison into `any`.
|
||||||
|
const lhs = target as number;
|
||||||
|
const rhs = source as number;
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case '<':
|
case '<':
|
||||||
return lhs < rhs;
|
return lhs < rhs;
|
||||||
|
|||||||
@@ -196,10 +196,7 @@ const resolveUnitReport = (unit: WarUnit): WarUnitReport => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildTraceUnitSnapshot = (
|
const buildTraceUnitSnapshot = (unit: WarUnit, defenderCity: City): WarBattleTraceUnitSnapshot => {
|
||||||
unit: WarUnit,
|
|
||||||
defenderCity: City
|
|
||||||
): WarBattleTraceUnitSnapshot => {
|
|
||||||
const common = {
|
const common = {
|
||||||
kind: unit instanceof WarUnitGeneral ? ('general' as const) : ('city' as const),
|
kind: unit instanceof WarUnitGeneral ? ('general' as const) : ('city' as const),
|
||||||
id: unit instanceof WarUnitGeneral ? unit.getGeneral().id : (unit as WarUnitCity).getCityId(),
|
id: unit instanceof WarUnitGeneral ? unit.getGeneral().id : (unit as WarUnitCity).getCityId(),
|
||||||
@@ -339,7 +336,6 @@ export const resolveWarBattle = <TriggerState extends GeneralTriggerState = Gene
|
|||||||
);
|
);
|
||||||
|
|
||||||
const iter = defenderUnits.values();
|
const iter = defenderUnits.values();
|
||||||
let defender: WarUnit<TriggerState> | null = null;
|
|
||||||
|
|
||||||
const getNextDefender = (
|
const getNextDefender = (
|
||||||
_prevDefender: WarUnit<TriggerState> | null,
|
_prevDefender: WarUnit<TriggerState> | null,
|
||||||
@@ -359,7 +355,7 @@ export const resolveWarBattle = <TriggerState extends GeneralTriggerState = Gene
|
|||||||
return candidate;
|
return candidate;
|
||||||
};
|
};
|
||||||
|
|
||||||
defender = getNextDefender(null, true);
|
let defender = getNextDefender(null, true);
|
||||||
let conquerCity = false;
|
let conquerCity = false;
|
||||||
let logWritten = false;
|
let logWritten = false;
|
||||||
let traceSeq = 0;
|
let traceSeq = 0;
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ describe('migrated general commands', () => {
|
|||||||
expect(updatedLord.experience).toBe(700);
|
expect(updatedLord.experience).toBe(700);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('che_증여: 최소 보유량을 넘는 자원만 이전한다', async () => {
|
it('che_증여: 금은 레거시 최소 보유량 0을 적용해 요청한 금액을 이전한다', async () => {
|
||||||
const actor = makeGeneral({ id: 1, nationId: 1, cityId: 1, name: '증여자', gold: 1300 });
|
const actor = makeGeneral({ id: 1, nationId: 1, cityId: 1, name: '증여자', gold: 1300 });
|
||||||
const dest = makeGeneral({ id: 2, nationId: 1, cityId: 1, name: '수령자', gold: 200 });
|
const dest = makeGeneral({ id: 2, nationId: 1, cityId: 1, name: '수령자', gold: 200 });
|
||||||
const nation = makeNation({ id: 1, name: '오', chiefGeneralId: 1, capitalCityId: 1, level: 1 });
|
const nation = makeNation({ id: 1, name: '오', chiefGeneralId: 1, capitalCityId: 1, level: 1 });
|
||||||
@@ -246,8 +246,8 @@ describe('migrated general commands', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(world.getGeneral(actor.id)!.gold).toBe(1000);
|
expect(world.getGeneral(actor.id)!.gold).toBe(800);
|
||||||
expect(world.getGeneral(dest.id)!.gold).toBe(500);
|
expect(world.getGeneral(dest.id)!.gold).toBe(700);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('che_해산: 방랑군 해산 시 세력과 소속을 정리한다', async () => {
|
it('che_해산: 방랑군 해산 시 세력과 소속을 정리한다', async () => {
|
||||||
|
|||||||
@@ -258,7 +258,6 @@ describe('General Commands New Scenario', () => {
|
|||||||
|
|
||||||
// 6. Retire (Needs age >= 60)
|
// 6. Retire (Needs age >= 60)
|
||||||
// Manually set age
|
// Manually set age
|
||||||
// Manually set age
|
|
||||||
const gToRetire = { ...g1_after_resign, age: 65 };
|
const gToRetire = { ...g1_after_resign, age: 65 };
|
||||||
world.snapshot.generals = world.snapshot.generals.map((g) => (g.id === 1 ? gToRetire : g));
|
world.snapshot.generals = world.snapshot.generals.map((g) => (g.id === 1 ? gToRetire : g));
|
||||||
const retireDef = retireSpec.createDefinition(systemEnv);
|
const retireDef = retireSpec.createDefinition(systemEnv);
|
||||||
@@ -274,7 +273,7 @@ describe('General Commands New Scenario', () => {
|
|||||||
const g1_after_retire = world.getGeneral(1)!;
|
const g1_after_retire = world.getGeneral(1)!;
|
||||||
expect(g1_after_retire.age).toBe(20);
|
expect(g1_after_retire.age).toBe(20);
|
||||||
// General::rebirth()는 앞선 명령으로 누적된 경험을 초기화하지 않고 절반으로 줄인다.
|
// General::rebirth()는 앞선 명령으로 누적된 경험을 초기화하지 않고 절반으로 줄인다.
|
||||||
expect(g1_after_retire.experience).toBe(142);
|
expect(g1_after_retire.experience).toBe(Math.round(gToRetire.experience * 0.5));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute employ and sabotage commands', async () => {
|
it('should execute employ and sabotage commands', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user