fix: restore legacy gameplay messages and record details
This commit is contained in:
@@ -230,6 +230,12 @@ export const generalRouter = router({
|
|||||||
injury: true,
|
injury: true,
|
||||||
experience: true,
|
experience: true,
|
||||||
dedication: true,
|
dedication: true,
|
||||||
|
age: true,
|
||||||
|
turnTime: true,
|
||||||
|
crewTypeId: true,
|
||||||
|
personalCode: true,
|
||||||
|
specialCode: true,
|
||||||
|
special2Code: true,
|
||||||
weaponCode: true,
|
weaponCode: true,
|
||||||
horseCode: true,
|
horseCode: true,
|
||||||
bookCode: true,
|
bookCode: true,
|
||||||
@@ -309,6 +315,19 @@ export const generalRouter = router({
|
|||||||
injury: general.injury,
|
injury: general.injury,
|
||||||
experience: general.experience,
|
experience: general.experience,
|
||||||
dedication: general.dedication,
|
dedication: general.dedication,
|
||||||
|
age: general.age,
|
||||||
|
turnTime: general.turnTime.toISOString(),
|
||||||
|
crewTypeId: general.crewTypeId,
|
||||||
|
traits: {
|
||||||
|
personal: general.personalCode,
|
||||||
|
specialWar: general.specialCode,
|
||||||
|
specialDomestic: general.special2Code,
|
||||||
|
},
|
||||||
|
progression: {
|
||||||
|
experienceLevel: readNumber(metaRecord.explevel, 0),
|
||||||
|
dedicationLevel: readNumber(metaRecord.dedlevel, 0),
|
||||||
|
dex: [1, 2, 3, 4, 5].map((index) => readNumber(metaRecord[`dex${index}`], 0)),
|
||||||
|
},
|
||||||
items: {
|
items: {
|
||||||
horse: normalizeItemCode(general.horseCode),
|
horse: normalizeItemCode(general.horseCode),
|
||||||
weapon: normalizeItemCode(general.weaponCode),
|
weapon: normalizeItemCode(general.weaponCode),
|
||||||
@@ -468,12 +487,12 @@ export const generalRouter = router({
|
|||||||
ctx.db.logEntry.findMany({
|
ctx.db.logEntry.findMany({
|
||||||
where: {
|
where: {
|
||||||
scope: LogScope.SYSTEM,
|
scope: LogScope.SYSTEM,
|
||||||
category: LogCategory.SUMMARY,
|
category: { in: [LogCategory.SUMMARY, LogCategory.ACTION] },
|
||||||
id: { gte: input.lastGeneralRecordId },
|
id: { gte: input.lastGeneralRecordId },
|
||||||
},
|
},
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
take,
|
take,
|
||||||
select: { id: true, text: true },
|
select: { id: true, text: true, createdAt: true },
|
||||||
}),
|
}),
|
||||||
ctx.db.logEntry.findMany({
|
ctx.db.logEntry.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -484,7 +503,7 @@ export const generalRouter = router({
|
|||||||
},
|
},
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
take,
|
take,
|
||||||
select: { id: true, text: true },
|
select: { id: true, text: true, createdAt: true },
|
||||||
}),
|
}),
|
||||||
ctx.db.logEntry.findMany({
|
ctx.db.logEntry.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -494,7 +513,7 @@ export const generalRouter = router({
|
|||||||
},
|
},
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
take,
|
take,
|
||||||
select: { id: true, text: true },
|
select: { id: true, text: true, createdAt: true },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -186,9 +186,14 @@ const zRevealMode = z.enum(['after_vote', 'after_end']);
|
|||||||
export const voteRouter = router({
|
export const voteRouter = router({
|
||||||
getVoteList: authedProcedure.query(async ({ ctx }) => {
|
getVoteList: authedProcedure.query(async ({ ctx }) => {
|
||||||
const worldState = await ctx.db.worldState.findFirst();
|
const worldState = await ctx.db.worldState.findFirst();
|
||||||
|
const worldMeta = asRecord(worldState?.meta ?? {});
|
||||||
const config = asRecord(worldState?.config ?? {});
|
const config = asRecord(worldState?.config ?? {});
|
||||||
const constValues = asRecord(config.const);
|
const constValues = asRecord(config.const);
|
||||||
const develCost = resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0);
|
const develCost = resolveNumber(
|
||||||
|
worldMeta,
|
||||||
|
['develcost', 'develCost'],
|
||||||
|
resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0)
|
||||||
|
);
|
||||||
const voteReward = develCost * 5;
|
const voteReward = develCost * 5;
|
||||||
|
|
||||||
const rows = await ctx.db.$queryRaw<VoteListRow[]>(GamePrisma.sql`
|
const rows = await ctx.db.$queryRaw<VoteListRow[]>(GamePrisma.sql`
|
||||||
@@ -404,12 +409,16 @@ export const voteRouter = router({
|
|||||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' });
|
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const worldMeta = asRecord(worldState.meta);
|
||||||
const config = asRecord(worldState.config);
|
const config = asRecord(worldState.config);
|
||||||
const constValues = asRecord(config.const);
|
const constValues = asRecord(config.const);
|
||||||
const develCost = resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0);
|
const develCost = resolveNumber(
|
||||||
|
worldMeta,
|
||||||
|
['develcost', 'develCost'],
|
||||||
|
resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0)
|
||||||
|
);
|
||||||
const voteReward = develCost * 5;
|
const voteReward = develCost * 5;
|
||||||
|
|
||||||
const worldMeta = asRecord(worldState.meta);
|
|
||||||
const scenarioMeta = asRecord(worldMeta.scenarioMeta);
|
const scenarioMeta = asRecord(worldMeta.scenarioMeta);
|
||||||
const startYear = readMetaNumber(scenarioMeta, 'startYear', worldState.currentYear);
|
const startYear = readMetaNumber(scenarioMeta, 'startYear', worldState.currentYear);
|
||||||
const initYear = readMetaNumber(worldMeta, 'initYear', startYear);
|
const initYear = readMetaNumber(worldMeta, 'initYear', startYear);
|
||||||
|
|||||||
@@ -241,10 +241,10 @@ describe('in-game my information ownership', () => {
|
|||||||
expect(fixture.db.logEntry.findMany).toHaveBeenNthCalledWith(
|
expect(fixture.db.logEntry.findMany).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
where: { scope: 'SYSTEM', category: 'SUMMARY', id: { gte: 0 } },
|
where: { scope: 'SYSTEM', category: { in: ['SUMMARY', 'ACTION'] }, id: { gte: 0 } },
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
take: 16,
|
take: 16,
|
||||||
select: { id: true, text: true },
|
select: { id: true, text: true, createdAt: true },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(fixture.db.logEntry.findMany).toHaveBeenNthCalledWith(
|
expect(fixture.db.logEntry.findMany).toHaveBeenNthCalledWith(
|
||||||
@@ -253,7 +253,7 @@ describe('in-game my information ownership', () => {
|
|||||||
where: { scope: 'GENERAL', category: 'ACTION', generalId: 7, id: { gte: 0 } },
|
where: { scope: 'GENERAL', category: 'ACTION', generalId: 7, id: { gte: 0 } },
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
take: 16,
|
take: 16,
|
||||||
select: { id: true, text: true },
|
select: { id: true, text: true, createdAt: true },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(fixture.db.logEntry.findMany).toHaveBeenNthCalledWith(
|
expect(fixture.db.logEntry.findMany).toHaveBeenNthCalledWith(
|
||||||
@@ -262,7 +262,7 @@ describe('in-game my information ownership', () => {
|
|||||||
where: { scope: 'SYSTEM', category: 'HISTORY', id: { gte: 0 } },
|
where: { scope: 'SYSTEM', category: 'HISTORY', id: { gte: 0 } },
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
take: 16,
|
take: 16,
|
||||||
select: { id: true, text: true },
|
select: { id: true, text: true, createdAt: true },
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ const auth: GameSessionTokenPayload = {
|
|||||||
type LogQuery = {
|
type LogQuery = {
|
||||||
where: {
|
where: {
|
||||||
scope: LogScope;
|
scope: LogScope;
|
||||||
category: LogCategory;
|
category: LogCategory | { in: LogCategory[] };
|
||||||
generalId?: number;
|
generalId?: number;
|
||||||
id: { gte: number };
|
id: { gte: number };
|
||||||
};
|
};
|
||||||
orderBy: { id: 'desc' };
|
orderBy: { id: 'desc' };
|
||||||
take: number;
|
take: number;
|
||||||
select: { id: true; text: true };
|
select: { id: true; text: true; createdAt: true };
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildContext = (findMany: (query: LogQuery) => Promise<Array<{ id: number; text: string }>>) =>
|
const buildContext = (findMany: (query: LogQuery) => Promise<Array<{ id: number; text: string }>>) =>
|
||||||
@@ -56,7 +56,7 @@ describe('general.getRecentRecords', () => {
|
|||||||
{ id: 20, text: '개인 cursor' },
|
{ id: 20, text: '개인 cursor' },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (query.where.category === LogCategory.SUMMARY) {
|
if (typeof query.where.category === 'object' && query.where.category.in.includes(LogCategory.SUMMARY)) {
|
||||||
return [
|
return [
|
||||||
{ id: 32, text: '장수 최신' },
|
{ id: 32, text: '장수 최신' },
|
||||||
{ id: 20, text: '장수 cursor' },
|
{ id: 20, text: '장수 cursor' },
|
||||||
@@ -89,7 +89,7 @@ describe('general.getRecentRecords', () => {
|
|||||||
},
|
},
|
||||||
orderBy: { id: 'desc' },
|
orderBy: { id: 'desc' },
|
||||||
take: 16,
|
take: 16,
|
||||||
select: { id: true, text: true },
|
select: { id: true, text: true, createdAt: true },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ const buildContext = (options: {
|
|||||||
voteRows?: Array<{ selection: number[]; cnt: number }>;
|
voteRows?: Array<{ selection: number[]; cnt: number }>;
|
||||||
pollRow?: typeof poll;
|
pollRow?: typeof poll;
|
||||||
configConst?: Record<string, unknown>;
|
configConst?: Record<string, unknown>;
|
||||||
|
metaDevelCost?: number;
|
||||||
auctionTargets?: string[];
|
auctionTargets?: string[];
|
||||||
}) => {
|
}) => {
|
||||||
const auth = options.auth === undefined ? buildAuth() : options.auth;
|
const auth = options.auth === undefined ? buildAuth() : options.auth;
|
||||||
@@ -136,6 +137,7 @@ const buildContext = (options: {
|
|||||||
tickSeconds: 3600,
|
tickSeconds: 3600,
|
||||||
config: { const: { develCost: 18, allItems: {}, ...(options.configConst ?? {}) } },
|
config: { const: { develCost: 18, allItems: {}, ...(options.configConst ?? {}) } },
|
||||||
meta: {
|
meta: {
|
||||||
|
...(options.metaDevelCost === undefined ? {} : { develcost: options.metaDevelCost }),
|
||||||
hiddenSeed: 'seed',
|
hiddenSeed: 'seed',
|
||||||
scenarioId: 200,
|
scenarioId: 200,
|
||||||
initYear: 180,
|
initYear: 180,
|
||||||
@@ -216,6 +218,16 @@ describe('vote router actor and permission boundaries', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the current world develcost for the legacy five-times survey reward', async () => {
|
||||||
|
const fixture = buildContext({ metaDevelCost: 30, configConst: { develCost: 0 } });
|
||||||
|
|
||||||
|
await expect(appRouter.createCaller(fixture.context).vote.getVoteList()).resolves.toMatchObject({
|
||||||
|
voteReward: 150,
|
||||||
|
});
|
||||||
|
await appRouter.createCaller(fixture.context).vote.submitVote({ voteId: 1, selection: [0] });
|
||||||
|
expect(fixture.requestCommand).toHaveBeenCalledWith(expect.objectContaining({ goldReward: 150 }));
|
||||||
|
});
|
||||||
|
|
||||||
it('includes active unique auctions in the API-side reward expectation', async () => {
|
it('includes active unique auctions in the API-side reward expectation', async () => {
|
||||||
const fixture = buildContext({
|
const fixture = buildContext({
|
||||||
configConst: {
|
configConst: {
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ const d징병 = 2;
|
|||||||
const d직전 = 3;
|
const d직전 = 3;
|
||||||
const d전쟁 = 4;
|
const d전쟁 = 4;
|
||||||
|
|
||||||
|
export const selectNpcMessageForTurn = (
|
||||||
|
message: unknown,
|
||||||
|
rng: Pick<RandUtil, 'nextBool'>,
|
||||||
|
frequencyPerDay: number,
|
||||||
|
turnTermMinutes: number
|
||||||
|
): string | null => {
|
||||||
|
if (!message) return null;
|
||||||
|
return rng.nextBool((frequencyPerDay * turnTermMinutes) / (60 * 24)) ? String(message) : null;
|
||||||
|
};
|
||||||
|
|
||||||
export const resolveLegacyAiStats = (
|
export const resolveLegacyAiStats = (
|
||||||
general: Pick<TurnGeneral, 'injury' | 'officerLevel' | 'stats'>,
|
general: Pick<TurnGeneral, 'injury' | 'officerLevel' | 'stats'>,
|
||||||
nation: Nation | null | undefined,
|
nation: Nation | null | undefined,
|
||||||
@@ -97,6 +107,7 @@ export class GeneralAI {
|
|||||||
public readonly env: ConstraintEnv;
|
public readonly env: ConstraintEnv;
|
||||||
public readonly startYear: number;
|
public readonly startYear: number;
|
||||||
public readonly turnTermMinutes: number;
|
public readonly turnTermMinutes: number;
|
||||||
|
private pendingNpcMessage: string | null = null;
|
||||||
|
|
||||||
public readonly aiConst: {
|
public readonly aiConst: {
|
||||||
baseGold: number;
|
baseGold: number;
|
||||||
@@ -213,9 +224,16 @@ export class GeneralAI {
|
|||||||
return (...args: unknown[]) => {
|
return (...args: unknown[]) => {
|
||||||
const result = Reflect.apply(value, receiver, args);
|
const result = Reflect.apply(value, receiver, args);
|
||||||
if (
|
if (
|
||||||
['nextFloat1', 'nextRangeInt', 'nextInt', 'nextBit', 'nextBool', 'choice', 'choiceUsingWeight', 'choiceUsingWeightPair'].includes(
|
[
|
||||||
String(property)
|
'nextFloat1',
|
||||||
)
|
'nextRangeInt',
|
||||||
|
'nextInt',
|
||||||
|
'nextBit',
|
||||||
|
'nextBool',
|
||||||
|
'choice',
|
||||||
|
'choiceUsingWeight',
|
||||||
|
'choiceUsingWeightPair',
|
||||||
|
].includes(String(property))
|
||||||
) {
|
) {
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`AI_RNG_TRACE ${JSON.stringify({
|
`AI_RNG_TRACE ${JSON.stringify({
|
||||||
@@ -330,11 +348,7 @@ export class GeneralAI {
|
|||||||
// Ref refreshes the cached AI state after these selected nation
|
// Ref refreshes the cached AI state after these selected nation
|
||||||
// commands, before choosing the general command with the same
|
// commands, before choosing the general command with the same
|
||||||
// RNG. The refresh includes another mixed-general type draw.
|
// RNG. The refresh includes another mixed-general type draw.
|
||||||
if (
|
if (['유저장긴급포상', 'NPC긴급포상', '선전포고', '천도'].includes(actionName)) {
|
||||||
['유저장긴급포상', 'NPC긴급포상', '선전포고', '천도'].includes(
|
|
||||||
actionName
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.reqUpdateInstance = true;
|
this.reqUpdateInstance = true;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -377,6 +391,12 @@ export class GeneralAI {
|
|||||||
return { set, unset };
|
return { set, unset };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consumeNpcMessage(): string | null {
|
||||||
|
const message = this.pendingNpcMessage;
|
||||||
|
this.pendingNpcMessage = null;
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
chooseGeneralTurn(reservedTurn: ReservedTurnEntry): AiCommandCandidate | null {
|
chooseGeneralTurn(reservedTurn: ReservedTurnEntry): AiCommandCandidate | null {
|
||||||
this.updateInstance();
|
this.updateInstance();
|
||||||
if (!this.worldRef) {
|
if (!this.worldRef) {
|
||||||
@@ -384,10 +404,12 @@ export class GeneralAI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const generalMeta = asRecord(this.general.meta);
|
const generalMeta = asRecord(this.general.meta);
|
||||||
const npcMessage = generalMeta.npcmsg ?? generalMeta.text;
|
this.pendingNpcMessage = selectNpcMessageForTurn(
|
||||||
if (npcMessage && this.rng.nextBool((this.aiConst.npcMessageFreqByDay * this.turnTermMinutes) / (60 * 24))) {
|
generalMeta.npcmsg ?? generalMeta.text,
|
||||||
// 메시지 영속화는 turn handler가 담당한다. 여기서는 레거시와 같은 RNG 소비를 보존한다.
|
this.rng,
|
||||||
}
|
this.aiConst.npcMessageFreqByDay,
|
||||||
|
this.turnTermMinutes
|
||||||
|
);
|
||||||
|
|
||||||
if (this.general.npcState >= 2) {
|
if (this.general.npcState >= 2) {
|
||||||
this.general.meta = { ...this.general.meta, defence_train: 80 };
|
this.general.meta = { ...this.general.meta, defence_train: 80 };
|
||||||
|
|||||||
@@ -1048,7 +1048,10 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
}
|
}
|
||||||
const actionContext = specificContext ?? baseContext;
|
const actionContext = specificContext ?? baseContext;
|
||||||
if ((process.env.CORE_AI_TRACE_GENERAL_IDS?.split(',') ?? []).includes(String(currentGeneral.id))) {
|
if ((process.env.CORE_AI_TRACE_GENERAL_IDS?.split(',') ?? []).includes(String(currentGeneral.id))) {
|
||||||
const tracedContext = actionContext as ActionContextBase & { destCity?: City; destGeneral?: TurnGeneral };
|
const tracedContext = actionContext as ActionContextBase & {
|
||||||
|
destCity?: City;
|
||||||
|
destGeneral?: TurnGeneral;
|
||||||
|
};
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`AI_ACTION_INPUT_TRACE ${JSON.stringify({ generalId: currentGeneral.id, kind, actionKey, actionArgs, destCityId: tracedContext.destCity?.id, destGeneralId: tracedContext.destGeneral?.id })}\n`
|
`AI_ACTION_INPUT_TRACE ${JSON.stringify({ generalId: currentGeneral.id, kind, actionKey, actionArgs, destCityId: tracedContext.destCity?.id, destGeneralId: tracedContext.destGeneral?.id })}\n`
|
||||||
);
|
);
|
||||||
@@ -1731,6 +1734,26 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
nationFallback,
|
nationFallback,
|
||||||
});
|
});
|
||||||
const candidate = ai.chooseGeneralTurn(generalCommand);
|
const candidate = ai.chooseGeneralTurn(generalCommand);
|
||||||
|
const npcMessage = ai.consumeNpcMessage();
|
||||||
|
if (npcMessage) {
|
||||||
|
const messageTarget = {
|
||||||
|
generalId: currentGeneral.id,
|
||||||
|
generalName: currentGeneral.name,
|
||||||
|
nationId: currentGeneral.nationId,
|
||||||
|
nationName: currentNation?.name ?? '재야',
|
||||||
|
color: currentNation?.color ?? '#000000',
|
||||||
|
icon: currentGeneral.picture ?? '',
|
||||||
|
};
|
||||||
|
messages.push({
|
||||||
|
msgType: 'public',
|
||||||
|
src: messageTarget,
|
||||||
|
dest: messageTarget,
|
||||||
|
text: npcMessage,
|
||||||
|
time: new Date(context.world.lastTurnTime),
|
||||||
|
validUntil: new Date('9999-12-31T00:00:00.000Z'),
|
||||||
|
option: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
generalAutorunMode =
|
generalAutorunMode =
|
||||||
candidate.action !== generalCommand.action ||
|
candidate.action !== generalCommand.action ||
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ describe('NPC 일반 내정 턴', () => {
|
|||||||
specialWar: null,
|
specialWar: null,
|
||||||
},
|
},
|
||||||
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||||
meta: { killturn: 999 },
|
meta: { killturn: 999, text: '기부는 저처럼 돈 많은 사람들이 많이 하면 됩니다' },
|
||||||
officerLevel: 4,
|
officerLevel: 4,
|
||||||
experience: 0,
|
experience: 0,
|
||||||
dedication: 0,
|
dedication: 0,
|
||||||
@@ -222,7 +222,7 @@ describe('NPC 일반 내정 턴', () => {
|
|||||||
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
||||||
iconPath: '',
|
iconPath: '',
|
||||||
map: {},
|
map: {},
|
||||||
const: {},
|
const: { npcMessageFreqByDay: 144 },
|
||||||
environment: { mapName: 'npc_domestic_map', unitSet: 'default' },
|
environment: { mapName: 'npc_domestic_map', unitSet: 'default' },
|
||||||
},
|
},
|
||||||
scenarioMeta: {
|
scenarioMeta: {
|
||||||
@@ -291,5 +291,12 @@ describe('NPC 일반 내정 턴', () => {
|
|||||||
security: 1063,
|
security: 1063,
|
||||||
});
|
});
|
||||||
expect(world.getGeneralById(1)!.turnTime.getTime()).toBe(addMinutes(mockDate, 10).getTime());
|
expect(world.getGeneralById(1)!.turnTime.getTime()).toBe(addMinutes(mockDate, 10).getTime());
|
||||||
|
expect(world.peekDirtyState().messages).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
msgType: 'public',
|
||||||
|
text: '기부는 저처럼 돈 많은 사람들이 많이 하면 됩니다',
|
||||||
|
src: expect.objectContaining({ generalId: 1, generalName: 'NPC_무장', nationId: 1 }),
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { selectNpcMessageForTurn } from '../src/turn/ai/generalAi/core.js';
|
||||||
|
|
||||||
|
describe('legacy NPC public chatter', () => {
|
||||||
|
it('uses the per-turn legacy probability and returns the scenario text', () => {
|
||||||
|
const nextBool = vi.fn(() => true);
|
||||||
|
|
||||||
|
expect(selectNpcMessageForTurn('기부는 저처럼 돈 많은 사람들이 많이 하면 됩니다', { nextBool }, 2, 10)).toBe(
|
||||||
|
'기부는 저처럼 돈 많은 사람들이 많이 하면 됩니다'
|
||||||
|
);
|
||||||
|
expect(nextBool).toHaveBeenCalledWith(2 / 144);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not consume RNG when a scenario NPC has no message', () => {
|
||||||
|
const nextBool = vi.fn(() => true);
|
||||||
|
expect(selectNpcMessageForTurn(null, { nextBool }, 2, 10)).toBeNull();
|
||||||
|
expect(nextBool).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -33,6 +33,7 @@ const safeSpanClasses = new Set([
|
|||||||
'war_type_attack',
|
'war_type_attack',
|
||||||
'war_type_defense',
|
'war_type_defense',
|
||||||
'war_type_siege',
|
'war_type_siege',
|
||||||
|
'hidden_but_copyable',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const escapeText = (value: string): string =>
|
const escapeText = (value: string): string =>
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ describe('formatLegacyLogHtml', () => {
|
|||||||
expect(formatLegacyLogHtml('<span class="unknown">미허용</span>')).toBe(
|
expect(formatLegacyLogHtml('<span class="unknown">미허용</span>')).toBe(
|
||||||
'<span class="unknown">미허용</span>'
|
'<span class="unknown">미허용</span>'
|
||||||
);
|
);
|
||||||
|
expect(formatLegacyLogHtml('<span class="hidden_but_copyable">(전투시드: fixed-seed)</span>')).toBe(
|
||||||
|
'<span class="hidden_but_copyable">(전투시드: fixed-seed)</span>'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps the fixed hex color form emitted by flag-change logs but rejects other inline CSS', () => {
|
it('keeps the fixed hex color form emitted by flag-change logs but rejects other inline CSS', () => {
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ export const resolveWarBattle = <TriggerState extends GeneralTriggerState = Gene
|
|||||||
const attackerNationName = (attackerUnit.getNationVar('name') as string | null) ?? 'UNKNOWN';
|
const attackerNationName = (attackerUnit.getNationVar('name') as string | null) ?? 'UNKNOWN';
|
||||||
const attackerName = attackerUnit.getName();
|
const attackerName = attackerUnit.getName();
|
||||||
const cityName = cityUnit.getName();
|
const cityName = cityUnit.getName();
|
||||||
const seedText = input.seed ? `(전투시드: ${input.seed})` : '';
|
const seedText = input.seed ? `<span class="hidden_but_copyable">(전투시드: ${input.seed})</span>` : '';
|
||||||
|
|
||||||
const josaRo = JosaUtil.pick(cityName, '로');
|
const josaRo = JosaUtil.pick(cityName, '로');
|
||||||
const josaYi = JosaUtil.pick(attackerName, '이');
|
const josaYi = JosaUtil.pick(attackerName, '이');
|
||||||
|
|||||||
Reference in New Issue
Block a user