merge: 이민족 첫 턴 시각 수정 반영
This commit is contained in:
@@ -290,7 +290,10 @@ const respondToRaiseInvader = async (options: {
|
|||||||
month: state.currentMonth,
|
month: state.currentMonth,
|
||||||
startyear: asNumber(state.meta.startYear, state.currentYear),
|
startyear: asNumber(state.meta.startYear, state.currentYear),
|
||||||
currentEventID: 0,
|
currentEventID: 0,
|
||||||
turnTime: now,
|
// Ref uses the frozen game_env.turntime while unification is paused.
|
||||||
|
// `now` is the realtime game projection and can be hours ahead after
|
||||||
|
// a long response wait, delaying every newly summoned invader turn.
|
||||||
|
turnTime: state.lastTurnTime,
|
||||||
},
|
},
|
||||||
event
|
event
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -314,10 +314,9 @@ export class AutorunNationPolicy {
|
|||||||
const stat = scenarioConfig.stat;
|
const stat = scenarioConfig.stat;
|
||||||
if (this.reqNpcWarGold === 0 || this.reqNpcWarRice === 0) {
|
if (this.reqNpcWarGold === 0 || this.reqNpcWarRice === 0) {
|
||||||
const crewType = findCrewTypeById(unitSet, env.defaultCrewTypeId);
|
const crewType = findCrewTypeById(unitSet, env.defaultCrewTypeId);
|
||||||
// Ref costWithTech() receives a troop count, not a leadership value.
|
// Ref costWithTech(tech, stat * 100) divides the troop count by 100.
|
||||||
// The policy funds four complete recruitments at the NPC stat cap.
|
const baseGold = crewType ? crewType.cost * getTechCost(tech, env.maxTechLevel) * stat.npcMax : 0;
|
||||||
const baseGold = crewType ? crewType.cost * getTechCost(tech, env.maxTechLevel) * stat.npcMax * 100 : 0;
|
const baseRice = crewType ? crewType.rice * getTechCost(tech, env.maxTechLevel) * stat.npcMax : 0;
|
||||||
const baseRice = crewType ? crewType.rice * getTechCost(tech, env.maxTechLevel) * stat.npcMax * 100 : 0;
|
|
||||||
if (this.reqNpcWarGold === 0) {
|
if (this.reqNpcWarGold === 0) {
|
||||||
this.reqNpcWarGold = roundTo(baseGold * 4, -2);
|
this.reqNpcWarGold = roundTo(baseGold * 4, -2);
|
||||||
}
|
}
|
||||||
@@ -328,8 +327,8 @@ export class AutorunNationPolicy {
|
|||||||
|
|
||||||
if (this.reqHumanWarUrgentGold === 0 || this.reqHumanWarUrgentRice === 0) {
|
if (this.reqHumanWarUrgentGold === 0 || this.reqHumanWarUrgentRice === 0) {
|
||||||
const crewType = findCrewTypeById(unitSet, env.defaultCrewTypeId);
|
const crewType = findCrewTypeById(unitSet, env.defaultCrewTypeId);
|
||||||
const baseGold = crewType ? crewType.cost * getTechCost(tech, env.maxTechLevel) * stat.max * 100 : 0;
|
const baseGold = crewType ? crewType.cost * getTechCost(tech, env.maxTechLevel) * stat.max : 0;
|
||||||
const baseRice = crewType ? crewType.rice * getTechCost(tech, env.maxTechLevel) * stat.max * 100 : 0;
|
const baseRice = crewType ? crewType.rice * getTechCost(tech, env.maxTechLevel) * stat.max : 0;
|
||||||
if (this.reqHumanWarUrgentGold === 0) {
|
if (this.reqHumanWarUrgentGold === 0) {
|
||||||
this.reqHumanWarUrgentGold = roundTo(baseGold * 6, -2);
|
this.reqHumanWarUrgentGold = roundTo(baseGold * 6, -2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ export const createRaiseInvaderHandler = (options: {
|
|||||||
|
|
||||||
world.updateWorldMeta({ isunited: 1, isUnited: 1 });
|
world.updateWorldMeta({ isunited: 1, isUnited: 1 });
|
||||||
const totalGeneralCount = npcEachCount * invaderCities.length + world.listGenerals().length;
|
const totalGeneralCount = npcEachCount * invaderCities.length + world.listGenerals().length;
|
||||||
|
let creationTurnBase = environment.turnTime;
|
||||||
const maxGeneralsPerMinute =
|
const maxGeneralsPerMinute =
|
||||||
options.maxGeneralsPerMinute ?? readNumber(world.getState().meta.maxGeneralsPerMinute, 1_000);
|
options.maxGeneralsPerMinute ?? readNumber(world.getState().meta.maxGeneralsPerMinute, 1_000);
|
||||||
const currentTurnMinutes = world.getState().tickSeconds / 60;
|
const currentTurnMinutes = world.getState().tickSeconds / 60;
|
||||||
@@ -166,6 +167,11 @@ export const createRaiseInvaderHandler = (options: {
|
|||||||
);
|
);
|
||||||
if (nextTerm !== undefined) {
|
if (nextTerm !== undefined) {
|
||||||
world.changeTurnTerm(nextTerm);
|
world.changeTurnTerm(nextTerm);
|
||||||
|
// Reprojection preserves the frozen monthly boundary by tick but
|
||||||
|
// changes its displayed Date. New generals must join that frozen
|
||||||
|
// boundary, not the realtime game clock that kept advancing while
|
||||||
|
// unification was waiting for a message response.
|
||||||
|
creationTurnBase = world.getState().lastTurnTime;
|
||||||
world.pushLog({
|
world.pushLog({
|
||||||
scope: LogScope.SYSTEM,
|
scope: LogScope.SYSTEM,
|
||||||
category: LogCategory.HISTORY,
|
category: LogCategory.HISTORY,
|
||||||
@@ -348,7 +354,7 @@ export const createRaiseInvaderHandler = (options: {
|
|||||||
reservedTurns: options.reservedTurns,
|
reservedTurns: options.reservedTurns,
|
||||||
env: options.env,
|
env: options.env,
|
||||||
rng,
|
rng,
|
||||||
environment,
|
environment: { ...environment, turnTime: creationTurnBase },
|
||||||
rawName: `${city.name}대왕`,
|
rawName: `${city.name}대왕`,
|
||||||
nationId,
|
nationId,
|
||||||
cityId: city.id,
|
cityId: city.id,
|
||||||
@@ -374,7 +380,7 @@ export const createRaiseInvaderHandler = (options: {
|
|||||||
reservedTurns: options.reservedTurns,
|
reservedTurns: options.reservedTurns,
|
||||||
env: options.env,
|
env: options.env,
|
||||||
rng,
|
rng,
|
||||||
environment,
|
environment: { ...environment, turnTime: creationTurnBase },
|
||||||
rawName: `${city.name}장수${index}`,
|
rawName: `${city.name}장수${index}`,
|
||||||
nationId,
|
nationId,
|
||||||
cityId: city.id,
|
cityId: city.id,
|
||||||
|
|||||||
@@ -277,6 +277,9 @@ describe('invader monthly actions', () => {
|
|||||||
it('moves a level-4 capital and scales turn times when the general limit requires a longer term', async () => {
|
it('moves a level-4 capital and scales turn times when the general limit requires a longer term', async () => {
|
||||||
const cities = [buildCity(1, 1, 4), buildCity(2, 1, 3), buildCity(3, 0, 4)];
|
const cities = [buildCity(1, 1, 4), buildCity(2, 1, 3), buildCity(3, 0, 4)];
|
||||||
const harness = buildHarness({ cities });
|
const harness = buildHarness({ cities });
|
||||||
|
// A message can be accepted long after the frozen monthly boundary.
|
||||||
|
// The realtime projection must not become the new invaders' first turn.
|
||||||
|
harness.environment.turnTime = new Date('0200-01-01T01:00:00.000Z');
|
||||||
const handler = createRaiseInvaderHandler({
|
const handler = createRaiseInvaderHandler({
|
||||||
getWorld: () => harness.world,
|
getWorld: () => harness.world,
|
||||||
reservedTurns: harness.reservedTurns,
|
reservedTurns: harness.reservedTurns,
|
||||||
@@ -290,6 +293,11 @@ describe('invader monthly actions', () => {
|
|||||||
expect(harness.world.getGeneralById(1)?.cityId).toBe(2);
|
expect(harness.world.getGeneralById(1)?.cityId).toBe(2);
|
||||||
expect(harness.world.getState().tickSeconds).toBe(30 * 60);
|
expect(harness.world.getState().tickSeconds).toBe(30 * 60);
|
||||||
expect(harness.world.getGeneralById(1)?.turnTime.toISOString()).toBe('0200-01-01T00:15:00.000Z');
|
expect(harness.world.getGeneralById(1)?.turnTime.toISOString()).toBe('0200-01-01T00:15:00.000Z');
|
||||||
|
const invaderTurnTimes = harness.world
|
||||||
|
.peekDirtyState()
|
||||||
|
.createdGenerals.map((general) => general.turnTime.getTime());
|
||||||
|
expect(Math.min(...invaderTurnTimes)).toBeGreaterThanOrEqual(new Date('0200-01-01T00:00:00.000Z').getTime());
|
||||||
|
expect(Math.max(...invaderTurnTimes)).toBeLessThan(new Date('0200-01-01T00:30:00.000Z').getTime());
|
||||||
expect(harness.world.peekDirtyState().logs[0]?.text).toBe('<R>★</>턴시간이 <C>30분</>으로 변경됩니다.');
|
expect(harness.world.peekDirtyState().logs[0]?.text).toBe('<R>★</>턴시간이 <C>30분</>으로 변경됩니다.');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -418,10 +426,7 @@ describe('invader monthly actions', () => {
|
|||||||
action: [['InvaderEnding']],
|
action: [['InvaderEnding']],
|
||||||
meta: {},
|
meta: {},
|
||||||
};
|
};
|
||||||
const invaderNations = [
|
const invaderNations = [buildNation(2, 1, 'ⓞ남만족'), buildNation(3, 2, 'ⓞ산월족')];
|
||||||
buildNation(2, 1, 'ⓞ남만족'),
|
|
||||||
buildNation(3, 2, 'ⓞ산월족'),
|
|
||||||
];
|
|
||||||
const harness = buildHarness({
|
const harness = buildHarness({
|
||||||
cities: [buildCity(1, 2, 4), buildCity(2, 3, 4)],
|
cities: [buildCity(1, 2, 4), buildCity(2, 3, 4)],
|
||||||
nations: [buildNeutralNation(), ...invaderNations],
|
nations: [buildNeutralNation(), ...invaderNations],
|
||||||
@@ -464,12 +469,7 @@ describe('invader monthly actions', () => {
|
|||||||
};
|
};
|
||||||
const harness = buildHarness({
|
const harness = buildHarness({
|
||||||
cities: [buildCity(1, 1, 3), buildCity(2, 2, 4), buildCity(3, 3, 4)],
|
cities: [buildCity(1, 1, 3), buildCity(2, 2, 4), buildCity(3, 3, 4)],
|
||||||
nations: [
|
nations: [buildNeutralNation(), buildNation(1), buildNation(2, 2, 'ⓞ남만족'), buildNation(3, 3, 'ⓞ산월족')],
|
||||||
buildNeutralNation(),
|
|
||||||
buildNation(1),
|
|
||||||
buildNation(2, 2, 'ⓞ남만족'),
|
|
||||||
buildNation(3, 3, 'ⓞ산월족'),
|
|
||||||
],
|
|
||||||
events: [endingEvent],
|
events: [endingEvent],
|
||||||
meta: { isunited: 1, refreshLimit: 3 },
|
meta: { isunited: 1, refreshLimit: 3 },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -176,32 +176,6 @@ const unitSet: UnitSetDefinition = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('NPC policy lifecycle', () => {
|
describe('NPC policy lifecycle', () => {
|
||||||
it('keeps hard-invader starting resources below the Ref-compatible NPC war funding target', () => {
|
|
||||||
const hardInvaderNation = {
|
|
||||||
...snapshot.nations[0]!,
|
|
||||||
name: 'ⓞ강족',
|
|
||||||
meta: { ...snapshot.nations[0]!.meta, tech: 15_000 },
|
|
||||||
};
|
|
||||||
const hardInvaderScenario = structuredClone(snapshot.scenarioConfig);
|
|
||||||
hardInvaderScenario.stat.npcMax = 80;
|
|
||||||
const hardInvaderGeneral = { ...general, npcState: 9, gold: 99_999, rice: 99_999 };
|
|
||||||
const policy = new AutorunNationPolicy({
|
|
||||||
general: hardInvaderGeneral,
|
|
||||||
aiOptions: null,
|
|
||||||
nationPolicy: null,
|
|
||||||
serverPolicy: null,
|
|
||||||
nation: hardInvaderNation,
|
|
||||||
env: { ...commandEnv, maxTechLevel: 12 },
|
|
||||||
scenarioConfig: hardInvaderScenario,
|
|
||||||
unitSet,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(policy.reqNpcWarGold).toBe(806_400);
|
|
||||||
expect(policy.reqNpcWarRice).toBe(806_400);
|
|
||||||
expect(hardInvaderGeneral.gold).toBeLessThan(policy.reqNpcWarGold);
|
|
||||||
expect(hardInvaderGeneral.rice).toBeLessThan(policy.reqNpcWarRice);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('applies CAS-protected semantic policy changes and the next AI instance consumes them without scheduler changes', () => {
|
it('applies CAS-protected semantic policy changes and the next AI instance consumes them without scheduler changes', () => {
|
||||||
const world = new InMemoryTurnWorld(state, snapshot, { schedule });
|
const world = new InMemoryTurnWorld(state, snapshot, { schedule });
|
||||||
const first = applyNpcPolicyMutation({
|
const first = applyNpcPolicyMutation({
|
||||||
@@ -280,10 +254,8 @@ describe('NPC policy lifecycle', () => {
|
|||||||
expect(policy.reqNationGold).toBe(4_321);
|
expect(policy.reqNationGold).toBe(4_321);
|
||||||
expect(policy.priority).toEqual(['천도']);
|
expect(policy.priority).toEqual(['천도']);
|
||||||
expect(policy.reqNpcDevelGold).toBe(540);
|
expect(policy.reqNpcDevelGold).toBe(540);
|
||||||
expect(policy.reqNpcWarGold).toBe(391_500);
|
expect(policy.reqNpcWarGold).toBe(3_900);
|
||||||
expect(policy.reqNpcWarRice).toBe(391_500);
|
expect(policy.reqNpcWarRice).toBe(3_900);
|
||||||
expect(policy.reqHumanWarUrgentGold).toBe(626_400);
|
|
||||||
expect(policy.reqHumanWarUrgentRice).toBe(626_400);
|
|
||||||
|
|
||||||
const beforeConflict = structuredClone(world.getNationById(1)?.meta);
|
const beforeConflict = structuredClone(world.getNationById(1)?.meta);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Reference in New Issue
Block a user