merge: 장수 런타임 레벨 상한 복구를 통합
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
|
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
|
||||||
import { normalizeScenarioEffect, type ScenarioEffectKey, type WarEngineConfig } from '@sammo-ts/logic';
|
import {
|
||||||
|
LEGACY_DEFAULT_MAX_LEVEL,
|
||||||
|
normalizeScenarioEffect,
|
||||||
|
type ScenarioEffectKey,
|
||||||
|
type WarEngineConfig,
|
||||||
|
} from '@sammo-ts/logic';
|
||||||
import { asRecord } from '@sammo-ts/common';
|
import { asRecord } from '@sammo-ts/common';
|
||||||
import type { UnitSetDefinition } from '@sammo-ts/logic';
|
import type { UnitSetDefinition } from '@sammo-ts/logic';
|
||||||
|
|
||||||
@@ -104,7 +109,7 @@ export const buildBattleSimEnvironment = async (
|
|||||||
maxAtmosByCommand: resolveNumber(constValues, ['maxAtmosByCommand'], DEFAULT_WAR_CONFIG.maxAtmosByCommand),
|
maxAtmosByCommand: resolveNumber(constValues, ['maxAtmosByCommand'], DEFAULT_WAR_CONFIG.maxAtmosByCommand),
|
||||||
maxTrainByWar: resolveNumber(constValues, ['maxTrainByWar'], DEFAULT_WAR_CONFIG.maxTrainByWar),
|
maxTrainByWar: resolveNumber(constValues, ['maxTrainByWar'], DEFAULT_WAR_CONFIG.maxTrainByWar),
|
||||||
maxAtmosByWar: resolveNumber(constValues, ['maxAtmosByWar'], DEFAULT_WAR_CONFIG.maxAtmosByWar),
|
maxAtmosByWar: resolveNumber(constValues, ['maxAtmosByWar'], DEFAULT_WAR_CONFIG.maxAtmosByWar),
|
||||||
maxGeneralStat: resolveNumber(constValues, ['maxLevel'], 255),
|
maxGeneralStat: resolveNumber(constValues, ['maxLevel'], LEGACY_DEFAULT_MAX_LEVEL),
|
||||||
statUpgradeLimit: resolveNumber(constValues, ['upgradeLimit'], 30),
|
statUpgradeLimit: resolveNumber(constValues, ['upgradeLimit'], 30),
|
||||||
castleCrewTypeId,
|
castleCrewTypeId,
|
||||||
armTypes: {
|
armTypes: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
import { asNumber, asRecord } from '@sammo-ts/common';
|
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||||
|
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic';
|
||||||
|
|
||||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||||
import { resolveDedicationLevelName, sanitizeInternalDisplayCode } from '../../../services/gameDisplayNames.js';
|
import { resolveDedicationLevelName, sanitizeInternalDisplayCode } from '../../../services/gameDisplayNames.js';
|
||||||
@@ -12,10 +13,10 @@ import {
|
|||||||
resolveNationPermission,
|
resolveNationPermission,
|
||||||
} from '../shared.js';
|
} from '../shared.js';
|
||||||
|
|
||||||
const experienceLevel = (experience: number): number =>
|
const experienceLevel = (experience: number, maxLevel: number): number =>
|
||||||
Math.max(
|
Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(100, experience < 1000 ? Math.floor(experience / 100) : Math.floor(Math.sqrt(experience / 10)))
|
Math.min(maxLevel, experience < 1000 ? Math.floor(experience / 100) : Math.floor(Math.sqrt(experience / 10)))
|
||||||
);
|
);
|
||||||
const dedicationLevel = (dedication: number, maxLevel: number): number =>
|
const dedicationLevel = (dedication: number, maxLevel: number): number =>
|
||||||
Math.max(0, Math.min(maxLevel, Math.ceil(Math.sqrt(dedication) / 10)));
|
Math.max(0, Math.min(maxLevel, Math.ceil(Math.sqrt(dedication) / 10)));
|
||||||
@@ -88,7 +89,9 @@ export const getGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
|
|||||||
const nationTrait = (await loadTraitNames([nation.typeCode], 'nation')).get(nation.typeCode);
|
const nationTrait = (await loadTraitNames([nation.typeCode], 'nation')).get(nation.typeCode);
|
||||||
const permission = resolveNationPermission(general, nation.meta, true);
|
const permission = resolveNationPermission(general, nation.meta, true);
|
||||||
const config = asRecord(worldState?.config);
|
const config = asRecord(worldState?.config);
|
||||||
const maxDedicationLevel = Math.max(0, Math.trunc(asNumber(asRecord(config.const).maxDedLevel, 30)));
|
const constValues = asRecord(config.const);
|
||||||
|
const maxExperienceLevel = Math.max(0, Math.trunc(asNumber(constValues.maxLevel, LEGACY_DEFAULT_MAX_LEVEL)));
|
||||||
|
const maxDedicationLevel = Math.max(0, Math.trunc(asNumber(constValues.maxDedLevel, 30)));
|
||||||
const visibleList = list.map((entry) => {
|
const visibleList = list.map((entry) => {
|
||||||
const entryDedicationLevel = dedicationLevel(entry.dedication, maxDedicationLevel);
|
const entryDedicationLevel = dedicationLevel(entry.dedication, maxDedicationLevel);
|
||||||
const dedicationDisplay = {
|
const dedicationDisplay = {
|
||||||
@@ -101,7 +104,7 @@ export const getGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
|
|||||||
return {
|
return {
|
||||||
...safeEntry,
|
...safeEntry,
|
||||||
refreshScoreTotal: accessByGeneral.get(entry.id) ?? 0,
|
refreshScoreTotal: accessByGeneral.get(entry.id) ?? 0,
|
||||||
experienceLevel: experienceLevel(entry.experience),
|
experienceLevel: experienceLevel(entry.experience, maxExperienceLevel),
|
||||||
...dedicationDisplay,
|
...dedicationDisplay,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -114,7 +117,7 @@ export const getGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
|
|||||||
troopName: null,
|
troopName: null,
|
||||||
officerCity: 0,
|
officerCity: 0,
|
||||||
officerCityName: null,
|
officerCityName: null,
|
||||||
experienceLevel: experienceLevel(entry.experience),
|
experienceLevel: experienceLevel(entry.experience, maxExperienceLevel),
|
||||||
...dedicationDisplay,
|
...dedicationDisplay,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
import { asRecord } from '@sammo-ts/common';
|
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||||
|
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic';
|
||||||
|
|
||||||
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
|
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
|
||||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||||
@@ -16,10 +17,10 @@ const readNumber = (record: Record<string, unknown>, keys: string[], fallback =
|
|||||||
};
|
};
|
||||||
const woundedStat = (value: number, injury: number): number =>
|
const woundedStat = (value: number, injury: number): number =>
|
||||||
injury > 0 ? Math.floor((value * (100 - injury)) / 100) : value;
|
injury > 0 ? Math.floor((value * (100 - injury)) / 100) : value;
|
||||||
const experienceLevel = (experience: number): number =>
|
const experienceLevel = (experience: number, maxLevel: number): number =>
|
||||||
Math.max(
|
Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(100, experience < 1000 ? Math.floor(experience / 100) : Math.floor(Math.sqrt(experience / 10)))
|
Math.min(maxLevel, experience < 1000 ? Math.floor(experience / 100) : Math.floor(Math.sqrt(experience / 10)))
|
||||||
);
|
);
|
||||||
const leadershipBonus = (officerLevel: number, nationLevel: number): number =>
|
const leadershipBonus = (officerLevel: number, nationLevel: number): number =>
|
||||||
officerLevel === 12 ? nationLevel * 2 : officerLevel >= 5 ? nationLevel : 0;
|
officerLevel === 12 ? nationLevel * 2 : officerLevel >= 5 ? nationLevel : 0;
|
||||||
@@ -55,6 +56,10 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
|
|||||||
ctx.db.worldState.findFirst({ select: { config: true } }),
|
ctx.db.worldState.findFirst({ select: { config: true } }),
|
||||||
]);
|
]);
|
||||||
const worldConfig = asRecord(worldState?.config);
|
const worldConfig = asRecord(worldState?.config);
|
||||||
|
const maxExperienceLevel = Math.max(
|
||||||
|
0,
|
||||||
|
Math.trunc(asNumber(asRecord(worldConfig.const).maxLevel, LEGACY_DEFAULT_MAX_LEVEL))
|
||||||
|
);
|
||||||
const environment = asRecord(worldConfig.environment ?? worldConfig.map);
|
const environment = asRecord(worldConfig.environment ?? worldConfig.map);
|
||||||
const unitSetName =
|
const unitSetName =
|
||||||
typeof environment.unitSet === 'string' && environment.unitSet.trim() ? environment.unitSet : ctx.profile.id;
|
typeof environment.unitSet === 'string' && environment.unitSet.trim() ? environment.unitSet : ctx.profile.id;
|
||||||
@@ -90,7 +95,7 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
|
|||||||
intelligence: woundedStat(general.intel, general.injury),
|
intelligence: woundedStat(general.intel, general.injury),
|
||||||
},
|
},
|
||||||
leadershipBonus: leadershipBonus(general.officerLevel, nation.level),
|
leadershipBonus: leadershipBonus(general.officerLevel, nation.level),
|
||||||
experienceLevel: experienceLevel(general.experience),
|
experienceLevel: experienceLevel(general.experience, maxExperienceLevel),
|
||||||
troopId: general.troopId,
|
troopId: general.troopId,
|
||||||
troopName: troopNames.get(general.troopId) ?? null,
|
troopName: troopNames.get(general.troopId) ?? null,
|
||||||
gold: general.gold,
|
gold: general.gold,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
import { asNumber, asRecord } from '@sammo-ts/common';
|
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||||
import { LogCategory, LogScope } from '@sammo-ts/logic';
|
import { LEGACY_DEFAULT_MAX_LEVEL, LogCategory, LogScope } from '@sammo-ts/logic';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { GameApiContext } from '../../context.js';
|
import type { GameApiContext } from '../../context.js';
|
||||||
@@ -573,7 +573,7 @@ export const publicRouter = router({
|
|||||||
const nationMap = new Map(nations.map((nation) => [nation.id, nation]));
|
const nationMap = new Map(nations.map((nation) => [nation.id, nation]));
|
||||||
const worldConfig = asRecord(worldState?.config);
|
const worldConfig = asRecord(worldState?.config);
|
||||||
const worldConstants = asRecord(worldConfig.const);
|
const worldConstants = asRecord(worldConfig.const);
|
||||||
const maxLevel = Math.max(0, Math.floor(asNumber(worldConstants.maxLevel, 255)));
|
const maxLevel = Math.max(0, Math.floor(asNumber(worldConstants.maxLevel, LEGACY_DEFAULT_MAX_LEVEL)));
|
||||||
const maxDedLevel = Math.max(0, Math.floor(asNumber(worldConstants.maxDedLevel, 30)));
|
const maxDedLevel = Math.max(0, Math.floor(asNumber(worldConstants.maxDedLevel, 30)));
|
||||||
|
|
||||||
// Legacy a_npcList.php shows select_pool humans first and possessed npc=1 rows.
|
// Legacy a_npcList.php shows select_pool humans first and possessed npc=1 rows.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { asRecord } from '@sammo-ts/common';
|
import { asRecord } from '@sammo-ts/common';
|
||||||
|
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { accessAuthedInputProcedure, authedProcedure } from '../../trpc.js';
|
import { accessAuthedInputProcedure, authedProcedure } from '../../trpc.js';
|
||||||
@@ -253,7 +254,7 @@ export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: z
|
|||||||
const accessMap = new Map(accessLogs.map((row) => [row.generalId, row.refreshScoreTotal]));
|
const accessMap = new Map(accessLogs.map((row) => [row.generalId, row.refreshScoreTotal]));
|
||||||
const config = asRecord(worldState?.config);
|
const config = asRecord(worldState?.config);
|
||||||
const constValues = asRecord(config.const);
|
const constValues = asRecord(config.const);
|
||||||
const maxLevel = readNumber(constValues.maxLevel, 255);
|
const maxLevel = readNumber(constValues.maxLevel, LEGACY_DEFAULT_MAX_LEVEL);
|
||||||
const maxDedLevel = readNumber(constValues.maxDedLevel, 30);
|
const maxDedLevel = readNumber(constValues.maxDedLevel, 30);
|
||||||
const worldMeta = asRecord(worldState?.meta);
|
const worldMeta = asRecord(worldState?.meta);
|
||||||
const isUnited = readNumber(worldMeta.isUnited ?? worldMeta.isunited) > 0;
|
const isUnited = readNumber(worldMeta.isUnited ?? worldMeta.isunited) > 0;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import type {
|
|||||||
TriggerValue,
|
TriggerValue,
|
||||||
UnitSetDefinition,
|
UnitSetDefinition,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import { evaluateConstraints } from '@sammo-ts/logic';
|
import { evaluateConstraints, LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic';
|
||||||
import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
|
import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
|
||||||
import { CommandResolver as RecruitmentCommandResolver } from '@sammo-ts/logic/actions/turn/general/che_징병.js';
|
import { CommandResolver as RecruitmentCommandResolver } from '@sammo-ts/logic/actions/turn/general/che_징병.js';
|
||||||
import { projectItemSlots, readItemInventoryFromMeta } from '@sammo-ts/logic/items/index.js';
|
import { projectItemSlots, readItemInventoryFromMeta } from '@sammo-ts/logic/items/index.js';
|
||||||
@@ -340,7 +340,7 @@ const buildCommandEnv = (worldState: WorldStateRow): CommandEnv => {
|
|||||||
defaultSpecialWar: resolveOptionalString(constValues, ['defaultSpecialWar']),
|
defaultSpecialWar: resolveOptionalString(constValues, ['defaultSpecialWar']),
|
||||||
initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], 0),
|
initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], 0),
|
||||||
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 12),
|
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 12),
|
||||||
maxStatLevel: resolveNumber(constValues, ['maxLevel'], 255),
|
maxStatLevel: resolveNumber(constValues, ['maxLevel'], LEGACY_DEFAULT_MAX_LEVEL),
|
||||||
techLevelIncYear: resolveNumber(constValues, ['techLevelIncYear'], 5),
|
techLevelIncYear: resolveNumber(constValues, ['techLevelIncYear'], 5),
|
||||||
initialAllowedTechLevel: resolveNumber(constValues, ['initialAllowedTechLevel'], 1),
|
initialAllowedTechLevel: resolveNumber(constValues, ['initialAllowedTechLevel'], 1),
|
||||||
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const token = (userId: string): GameSessionTokenPayload => ({
|
|||||||
user: { id: userId, username: userId, displayName: userId, roles: [] },
|
user: { id: userId, username: userId, displayName: userId, roles: [] },
|
||||||
sanctions: {},
|
sanctions: {},
|
||||||
});
|
});
|
||||||
const fixture = (generals: GeneralRow[], userId = 'u1') => {
|
const fixture = (generals: GeneralRow[], userId = 'u1', maxLevel?: number) => {
|
||||||
const db = {
|
const db = {
|
||||||
general: {
|
general: {
|
||||||
findFirst: vi.fn(async ({ where }: { where: { userId: string } }) =>
|
findFirst: vi.fn(async ({ where }: { where: { userId: string } }) =>
|
||||||
@@ -83,7 +83,9 @@ const fixture = (generals: GeneralRow[], userId = 'u1') => {
|
|||||||
},
|
},
|
||||||
city: { findMany: vi.fn(async () => [{ id: 1, name: '업' }]) },
|
city: { findMany: vi.fn(async () => [{ id: 1, name: '업' }]) },
|
||||||
troop: { findMany: vi.fn(async () => [{ troopLeaderId: 2, name: '선봉대' }]) },
|
troop: { findMany: vi.fn(async () => [{ troopLeaderId: 2, name: '선봉대' }]) },
|
||||||
worldState: { findFirst: vi.fn(async () => null) },
|
worldState: {
|
||||||
|
findFirst: vi.fn(async () => ({ config: { const: maxLevel === undefined ? {} : { maxLevel } } })),
|
||||||
|
},
|
||||||
generalTurn: {
|
generalTurn: {
|
||||||
findMany: vi.fn(async () => [
|
findMany: vi.fn(async () => [
|
||||||
{
|
{
|
||||||
@@ -118,7 +120,7 @@ const fixture = (generals: GeneralRow[], userId = 'u1') => {
|
|||||||
|
|
||||||
describe('nation general and secret office permissions', () => {
|
describe('nation general and secret office permissions', () => {
|
||||||
it('redacts ordinary-member details and denies the secret office', async () => {
|
it('redacts ordinary-member details and denies the secret office', async () => {
|
||||||
const { caller } = fixture([general()]);
|
const { caller } = fixture([general({ experience: 144_000 })]);
|
||||||
const result = await caller.nation.getGeneralList();
|
const result = await caller.nation.getGeneralList();
|
||||||
expect(result.viewer).toEqual({ generalId: 1, permission: 0 });
|
expect(result.viewer).toEqual({ generalId: 1, permission: 0 });
|
||||||
expect(result.generals[0]).toMatchObject({
|
expect(result.generals[0]).toMatchObject({
|
||||||
@@ -129,6 +131,7 @@ describe('nation general and secret office permissions', () => {
|
|||||||
dedicationLevel: 1,
|
dedicationLevel: 1,
|
||||||
dedicationText: '30품관',
|
dedicationText: '30품관',
|
||||||
bill: 600,
|
bill: 600,
|
||||||
|
experienceLevel: 120,
|
||||||
});
|
});
|
||||||
expect(result.generals[0]).not.toHaveProperty('crew');
|
expect(result.generals[0]).not.toHaveProperty('crew');
|
||||||
await expect(caller.nation.getSecretGeneralList()).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
await expect(caller.nation.getSecretGeneralList()).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
||||||
@@ -136,12 +139,21 @@ describe('nation general and secret office permissions', () => {
|
|||||||
it('uses the session-owned general and scopes secret rows to that nation', async () => {
|
it('uses the session-owned general and scopes secret rows to that nation', async () => {
|
||||||
const first = general();
|
const first = general();
|
||||||
const actor = general({ id: 2, userId: 'u2', officerLevel: 5, meta: { belong: 1 } });
|
const actor = general({ id: 2, userId: 'u2', officerLevel: 5, meta: { belong: 1 } });
|
||||||
const ally = general({ id: 3, userId: 'u3', gold: 3000, crew: 200, train: 80, atmos: 80 });
|
const ally = general({
|
||||||
|
id: 3,
|
||||||
|
userId: 'u3',
|
||||||
|
gold: 3000,
|
||||||
|
crew: 200,
|
||||||
|
train: 80,
|
||||||
|
atmos: 80,
|
||||||
|
experience: 400_000,
|
||||||
|
});
|
||||||
const foreign = general({ id: 4, userId: 'u4', nationId: 2, gold: 99999 });
|
const foreign = general({ id: 4, userId: 'u4', nationId: 2, gold: 99999 });
|
||||||
const { caller, db } = fixture([first, actor, ally, foreign], 'u2');
|
const { caller, db } = fixture([first, actor, ally, foreign], 'u2');
|
||||||
const result = await caller.nation.getSecretGeneralList();
|
const result = await caller.nation.getSecretGeneralList();
|
||||||
expect(result.viewer).toEqual({ generalId: 2, permission: 2 });
|
expect(result.viewer).toEqual({ generalId: 2, permission: 2 });
|
||||||
expect(result.generals.map((g) => g.id)).toEqual([1, 2, 3]);
|
expect(result.generals.map((g) => g.id)).toEqual([1, 2, 3]);
|
||||||
|
expect(result.generals.find((entry) => entry.id === 3)?.experienceLevel).toBe(200);
|
||||||
expect(result.summary).toMatchObject({ gold: 5000, crew: 800, generalCount: 3 });
|
expect(result.summary).toMatchObject({ gold: 5000, crew: 800, generalCount: 3 });
|
||||||
expect(result.generals[0]?.reservedCommands).toEqual([
|
expect(result.generals[0]?.reservedCommands).toEqual([
|
||||||
{ action: 'che_징병', args: { crewType: 1, amount: 300 } },
|
{ action: 'che_징병', args: { crewType: 1, amount: 300 } },
|
||||||
@@ -159,4 +171,15 @@ describe('nation general and secret office permissions', () => {
|
|||||||
const { caller } = fixture([penalized]);
|
const { caller } = fixture([penalized]);
|
||||||
await expect(caller.nation.getSecretGeneralList()).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
await expect(caller.nation.getSecretGeneralList()).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
||||||
});
|
});
|
||||||
|
it('honors an explicit Ref maxLevel override for projected experience levels', async () => {
|
||||||
|
const actor = general({ officerLevel: 5, experience: 400_000 });
|
||||||
|
const { caller } = fixture([actor], 'u1', 150);
|
||||||
|
|
||||||
|
const [generalList, secretList] = await Promise.all([
|
||||||
|
caller.nation.getGeneralList(),
|
||||||
|
caller.nation.getSecretGeneralList(),
|
||||||
|
]);
|
||||||
|
expect(generalList.generals[0]?.experienceLevel).toBe(150);
|
||||||
|
expect(secretList.generals[0]?.experienceLevel).toBe(150);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
TurnCommandEnv,
|
TurnCommandEnv,
|
||||||
UnitSetDefinition,
|
UnitSetDefinition,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import { evaluateConstraints } from '@sammo-ts/logic';
|
import { evaluateConstraints, LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic';
|
||||||
import type { ConstraintContext } from '@sammo-ts/logic';
|
import type { ConstraintContext } from '@sammo-ts/logic';
|
||||||
import { GAME_TICKS_PER_TURN, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
import { GAME_TICKS_PER_TURN, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||||
import { simpleSerialize } from '@sammo-ts/logic/war/utils.js';
|
import { simpleSerialize } from '@sammo-ts/logic/war/utils.js';
|
||||||
@@ -872,17 +872,14 @@ export class GeneralAI {
|
|||||||
? resolveLegacyAiStatsWithModules(
|
? resolveLegacyAiStatsWithModules(
|
||||||
candidate,
|
candidate,
|
||||||
this.nation,
|
this.nation,
|
||||||
this.commandEnv.maxStatLevel ?? this.scenarioConfig.stat.max,
|
this.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL,
|
||||||
this.commandEnv.generalActionModules,
|
this.commandEnv.generalActionModules,
|
||||||
this.worldRef,
|
this.worldRef,
|
||||||
this.world,
|
this.world,
|
||||||
this.startYear
|
this.startYear
|
||||||
).fullLeadership
|
).fullLeadership
|
||||||
: resolveLegacyAiStats(
|
: resolveLegacyAiStats(candidate, this.nation, this.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL)
|
||||||
candidate,
|
.fullLeadership;
|
||||||
this.nation,
|
|
||||||
this.commandEnv.maxStatLevel ?? this.scenarioConfig.stat.max
|
|
||||||
).fullLeadership;
|
|
||||||
if (fullLeadership >= this.nationPolicy.minNpcWarLeadership) {
|
if (fullLeadership >= this.nationPolicy.minNpcWarLeadership) {
|
||||||
npcWarGenerals[candidate.id] = candidate;
|
npcWarGenerals[candidate.id] = candidate;
|
||||||
} else {
|
} else {
|
||||||
@@ -1055,17 +1052,13 @@ export class GeneralAI {
|
|||||||
? resolveLegacyAiStatsWithModules(
|
? resolveLegacyAiStatsWithModules(
|
||||||
this.general,
|
this.general,
|
||||||
this.nation,
|
this.nation,
|
||||||
this.commandEnv.maxStatLevel ?? this.scenarioConfig.stat.max,
|
this.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL,
|
||||||
this.commandEnv.generalActionModules,
|
this.commandEnv.generalActionModules,
|
||||||
this.worldRef,
|
this.worldRef,
|
||||||
this.world,
|
this.world,
|
||||||
this.startYear
|
this.startYear
|
||||||
)
|
)
|
||||||
: resolveLegacyAiStats(
|
: resolveLegacyAiStats(this.general, this.nation, this.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL);
|
||||||
this.general,
|
|
||||||
this.nation,
|
|
||||||
this.commandEnv.maxStatLevel ?? this.scenarioConfig.stat.max
|
|
||||||
);
|
|
||||||
this.general.meta = {
|
this.general.meta = {
|
||||||
...this.general.meta,
|
...this.general.meta,
|
||||||
...stats,
|
...stats,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { GeneralAI } from '../core.js';
|
import type { GeneralAI } from '../core.js';
|
||||||
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
|
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
|
||||||
|
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic/scenario/constants.js';
|
||||||
import { findCrewTypeById, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
|
import { findCrewTypeById, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
|
||||||
import type { TurnGeneral } from '../../../types.js';
|
import type { TurnGeneral } from '../../../types.js';
|
||||||
import { asRecord, readMetaNumber, readRequiredMetaNumber } from '../../aiUtils.js';
|
import { asRecord, readMetaNumber, readRequiredMetaNumber } from '../../aiUtils.js';
|
||||||
@@ -72,12 +73,12 @@ const getFullLeadership = (ai: GeneralAI, general: TurnGeneral): number => {
|
|||||||
'leadership',
|
'leadership',
|
||||||
general.stats.leadership
|
general.stats.leadership
|
||||||
);
|
);
|
||||||
const maxStat = ai.commandEnv.maxStatLevel ?? ai.scenarioConfig.stat.max;
|
const maxStat = ai.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL;
|
||||||
return Math.trunc(Math.max(0, Math.min(Number(adjusted), maxStat)));
|
return Math.trunc(Math.max(0, Math.min(Number(adjusted), maxStat)));
|
||||||
}
|
}
|
||||||
const nationLevel = ai.nation?.level ?? 0;
|
const nationLevel = ai.nation?.level ?? 0;
|
||||||
const officerBonus = general.officerLevel === 12 ? nationLevel * 2 : general.officerLevel >= 5 ? nationLevel : 0;
|
const officerBonus = general.officerLevel === 12 ? nationLevel * 2 : general.officerLevel >= 5 ? nationLevel : 0;
|
||||||
const maxStat = ai.commandEnv.maxStatLevel ?? ai.scenarioConfig.stat.max;
|
const maxStat = ai.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL;
|
||||||
return Math.max(0, Math.min(general.stats.leadership + officerBonus, maxStat));
|
return Math.max(0, Math.min(general.stats.leadership + officerBonus, maxStat));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
LEGACY_RANDOM_GENERAL_FIRST_NAMES,
|
LEGACY_RANDOM_GENERAL_FIRST_NAMES,
|
||||||
LEGACY_RANDOM_GENERAL_LAST_NAMES,
|
LEGACY_RANDOM_GENERAL_LAST_NAMES,
|
||||||
|
LEGACY_DEFAULT_MAX_LEVEL,
|
||||||
loadGeneralTurnCommandSpecs,
|
loadGeneralTurnCommandSpecs,
|
||||||
loadNationTurnCommandSpecs,
|
loadNationTurnCommandSpecs,
|
||||||
loadActionModuleBundle,
|
loadActionModuleBundle,
|
||||||
@@ -132,7 +133,9 @@ export const buildCommandEnv = (config: ScenarioConfig, unitSet?: UnitSetDefinit
|
|||||||
]),
|
]),
|
||||||
initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], DEFAULT_INITIAL_NATION_GEN_LIMIT),
|
initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], DEFAULT_INITIAL_NATION_GEN_LIMIT),
|
||||||
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], DEFAULT_MAX_TECH_LEVEL),
|
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], DEFAULT_MAX_TECH_LEVEL),
|
||||||
maxStatLevel: resolveNumber(constValues, ['maxLevel'], config.stat.max),
|
// `stat.max` bounds join-time allocation (80 by default), while Ref
|
||||||
|
// runtime stat calculations use GameConst::$maxLevel.
|
||||||
|
maxStatLevel: resolveNumber(constValues, ['maxLevel'], LEGACY_DEFAULT_MAX_LEVEL),
|
||||||
maxDedicationLevel: resolveNumber(constValues, ['maxDedLevel'], 30),
|
maxDedicationLevel: resolveNumber(constValues, ['maxDedLevel'], 30),
|
||||||
statUpgradeLimit: resolveNumber(constValues, ['upgradeLimit'], 30),
|
statUpgradeLimit: resolveNumber(constValues, ['upgradeLimit'], 30),
|
||||||
techLevelIncYear: resolveNumber(constValues, ['techLevelIncYear'], 5),
|
techLevelIncYear: resolveNumber(constValues, ['techLevelIncYear'], 5),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
rollUniqueLottery,
|
rollUniqueLottery,
|
||||||
getNextTurnAt,
|
getNextTurnAt,
|
||||||
getBillByLevel,
|
getBillByLevel,
|
||||||
|
LEGACY_DEFAULT_MAX_LEVEL,
|
||||||
type ItemModule,
|
type ItemModule,
|
||||||
type UniqueLotteryRunner,
|
type UniqueLotteryRunner,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
@@ -126,7 +127,7 @@ export const applyLegacyGeneralProgression = (
|
|||||||
env: TurnCommandEnv,
|
env: TurnCommandEnv,
|
||||||
logs: LogEntryDraft[]
|
logs: LogEntryDraft[]
|
||||||
): TurnGeneral => {
|
): TurnGeneral => {
|
||||||
const maxStatLevel = env.maxStatLevel ?? 255;
|
const maxStatLevel = env.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL;
|
||||||
const maxDedicationLevel = env.maxDedicationLevel ?? 30;
|
const maxDedicationLevel = env.maxDedicationLevel ?? 30;
|
||||||
const expLevel = Math.max(
|
const expLevel = Math.max(
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { TurnSchedule } from '@sammo-ts/logic/turn/calendar.js';
|
|||||||
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||||
import { createTurnTestHarness } from './helpers/turnTestHarness.js';
|
import { createTurnTestHarness } from './helpers/turnTestHarness.js';
|
||||||
import { applyLegacyGeneralProgression } from '../src/turn/reservedTurnHandler.js';
|
import { applyLegacyGeneralProgression } from '../src/turn/reservedTurnHandler.js';
|
||||||
|
import { buildCommandEnv } from '../src/turn/reservedTurnCommands.js';
|
||||||
|
|
||||||
const start = new Date('0200-01-01T00:00:00.000Z');
|
const start = new Date('0200-01-01T00:00:00.000Z');
|
||||||
const schedule: TurnSchedule = { entries: [{ startMinute: 0, tickMinutes: 10 }] };
|
const schedule: TurnSchedule = { entries: [{ startMinute: 0, tickMinutes: 10 }] };
|
||||||
@@ -154,6 +155,21 @@ const makeState = (): TurnWorldState => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('legacy general-turn execution contract', () => {
|
describe('legacy general-turn execution contract', () => {
|
||||||
|
it('does not reuse the join stat allocation maximum as the runtime level cap', () => {
|
||||||
|
const previous = makeGeneral({
|
||||||
|
experience: 144_000,
|
||||||
|
meta: { killturn: 24, explevel: 120 },
|
||||||
|
});
|
||||||
|
const afterRecruitment = makeGeneral({
|
||||||
|
experience: 144_001,
|
||||||
|
meta: { killturn: 24, explevel: 120 },
|
||||||
|
});
|
||||||
|
const env = buildCommandEnv(makeSnapshot(previous).scenarioConfig);
|
||||||
|
|
||||||
|
expect(env.maxStatLevel).toBe(255);
|
||||||
|
expect(applyLegacyGeneralProgression(afterRecruitment, previous, 'che_모병', env, []).meta.explevel).toBe(120);
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves the battle-computed level across legacy INT rounding', () => {
|
it('preserves the battle-computed level across legacy INT rounding', () => {
|
||||||
const previous = makeGeneral({
|
const previous = makeGeneral({
|
||||||
experience: 6_700,
|
experience: 6_700,
|
||||||
|
|||||||
@@ -92,4 +92,17 @@ describe('tracked scenario resources', () => {
|
|||||||
expect([...secretScenarioKeys!].filter((key) => key.startsWith('event_전투특기_'))).toHaveLength(20);
|
expect([...secretScenarioKeys!].filter((key) => key.startsWith('event_전투특기_'))).toHaveLength(20);
|
||||||
expect(secretScenarioKeys?.has('event_전투특기_격노')).toBe(true);
|
expect(secretScenarioKeys?.has('event_전투특기_격노')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps join allocation bounds separate from the Ref runtime stat level limit', async () => {
|
||||||
|
const scenario = await loadScenarioDefinitionById(1);
|
||||||
|
|
||||||
|
expect(scenario.config.stat.max).toBe(80);
|
||||||
|
expect(buildCommandEnv(scenario.config).maxStatLevel).toBe(255);
|
||||||
|
expect(
|
||||||
|
buildCommandEnv({
|
||||||
|
...scenario.config,
|
||||||
|
const: { ...scenario.config.const, maxLevel: 512 },
|
||||||
|
}).maxStatLevel
|
||||||
|
).toBe(512);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { General } from '@sammo-ts/logic/domain/entities.js';
|
import type { General } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
|
import type { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
|
||||||
|
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic/scenario/constants.js';
|
||||||
import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
|
import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
|
||||||
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
|
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
|
||||||
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||||
@@ -208,7 +209,7 @@ export const buildWarConfig = (scenarioConfig: ScenarioConfig, unitSet: UnitSetD
|
|||||||
maxAtmosByCommand: resolveNumber(constValues, ['maxAtmosByCommand'], DEFAULT_WAR_CONFIG.maxAtmosByCommand),
|
maxAtmosByCommand: resolveNumber(constValues, ['maxAtmosByCommand'], DEFAULT_WAR_CONFIG.maxAtmosByCommand),
|
||||||
maxTrainByWar: resolveNumber(constValues, ['maxTrainByWar'], DEFAULT_WAR_CONFIG.maxTrainByWar),
|
maxTrainByWar: resolveNumber(constValues, ['maxTrainByWar'], DEFAULT_WAR_CONFIG.maxTrainByWar),
|
||||||
maxAtmosByWar: resolveNumber(constValues, ['maxAtmosByWar'], DEFAULT_WAR_CONFIG.maxAtmosByWar),
|
maxAtmosByWar: resolveNumber(constValues, ['maxAtmosByWar'], DEFAULT_WAR_CONFIG.maxAtmosByWar),
|
||||||
maxGeneralStat: resolveNumber(constValues, ['maxLevel'], 255),
|
maxGeneralStat: resolveNumber(constValues, ['maxLevel'], LEGACY_DEFAULT_MAX_LEVEL),
|
||||||
statUpgradeLimit: resolveNumber(constValues, ['upgradeLimit'], 30),
|
statUpgradeLimit: resolveNumber(constValues, ['upgradeLimit'], 30),
|
||||||
castleCrewTypeId,
|
castleCrewTypeId,
|
||||||
armTypes: {
|
armTypes: {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
|||||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
import type { GeneralTurnCommandSpec } from './index.js';
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||||
|
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic/scenario/constants.js';
|
||||||
|
|
||||||
const ACTION_NAME = '등용수락';
|
const ACTION_NAME = '등용수락';
|
||||||
const ACTION_KEY = 'che_등용수락';
|
const ACTION_KEY = 'che_등용수락';
|
||||||
@@ -103,7 +104,7 @@ export class ActionResolver<
|
|||||||
const recruiterExpLevel = Math.max(
|
const recruiterExpLevel = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(
|
Math.min(
|
||||||
this.env.maxStatLevel ?? 255,
|
this.env.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL,
|
||||||
recruiterExperience < 1_000
|
recruiterExperience < 1_000
|
||||||
? Math.trunc(recruiterExperience / 100)
|
? Math.trunc(recruiterExperience / 100)
|
||||||
: Math.trunc(Math.sqrt(recruiterExperience / 10))
|
: Math.trunc(Math.sqrt(recruiterExperience / 10))
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
|
|||||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
||||||
|
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic/scenario/constants.js';
|
||||||
import type { GeneralTurnCommandSpec } from './index.js';
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface ProcureArgs {}
|
export interface ProcureArgs {}
|
||||||
@@ -32,7 +33,10 @@ export const roundLegacyAccumulatedInteger = (current: number, delta: number): n
|
|||||||
export const resolveLegacyExperienceLevel = (experience: number): number =>
|
export const resolveLegacyExperienceLevel = (experience: number): number =>
|
||||||
Math.max(
|
Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(255, experience < 1_000 ? Math.trunc(experience / 100) : Math.trunc(Math.sqrt(experience / 10)))
|
Math.min(
|
||||||
|
LEGACY_DEFAULT_MAX_LEVEL,
|
||||||
|
experience < 1_000 ? Math.trunc(experience / 100) : Math.trunc(Math.sqrt(experience / 10))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
export const resolveLegacyDedicationLevel = (dedication: number): number =>
|
export const resolveLegacyDedicationLevel = (dedication: number): number =>
|
||||||
Math.max(0, Math.min(30, Math.ceil(Math.sqrt(dedication) / 10)));
|
Math.max(0, Math.min(30, Math.ceil(Math.sqrt(dedication) / 10)));
|
||||||
@@ -65,7 +69,7 @@ export class ActionResolver<
|
|||||||
const rawLeadership = general.stats.leadership * injuryMultiplier;
|
const rawLeadership = general.stats.leadership * injuryMultiplier;
|
||||||
const rawStrength = general.stats.strength * injuryMultiplier;
|
const rawStrength = general.stats.strength * injuryMultiplier;
|
||||||
const rawIntelligence = general.stats.intelligence * injuryMultiplier;
|
const rawIntelligence = general.stats.intelligence * injuryMultiplier;
|
||||||
const maxStat = 255;
|
const maxStat = LEGACY_DEFAULT_MAX_LEVEL;
|
||||||
const legacyStat = (stat: 'leadership' | 'strength' | 'intelligence', value: number): number =>
|
const legacyStat = (stat: 'leadership' | 'strength' | 'intelligence', value: number): number =>
|
||||||
Math.trunc(Math.max(0, Math.min(maxStat, this.pipeline.onCalcStat(context, stat, value))));
|
Math.trunc(Math.max(0, Math.min(maxStat, this.pipeline.onCalcStat(context, stat, value))));
|
||||||
let score =
|
let score =
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import type {
|
|||||||
ActionResolveContext,
|
ActionResolveContext,
|
||||||
} from '@sammo-ts/logic/actions/turn/actionContext.js';
|
} from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
||||||
|
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic/scenario/constants.js';
|
||||||
import type { GeneralTurnCommandSpec } from './index.js';
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
import { clamp } from 'es-toolkit';
|
import { clamp } from 'es-toolkit';
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralT
|
|||||||
} else if (this.config.statKey === 'intelligence') {
|
} else if (this.config.statKey === 'intelligence') {
|
||||||
rawStats.intelligence += Math.round(rawStats.strength / 4);
|
rawStats.intelligence += Math.round(rawStats.strength / 4);
|
||||||
}
|
}
|
||||||
const maxStatLevel = this.env.maxStatLevel ?? 255;
|
const maxStatLevel = this.env.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL;
|
||||||
let score = clamp(rawStats[this.config.statKey], 0, maxStatLevel);
|
let score = clamp(rawStats[this.config.statKey], 0, maxStatLevel);
|
||||||
score = this.pipeline.onCalcStat(context, this.config.statKey, score);
|
score = this.pipeline.onCalcStat(context, this.config.statKey, score);
|
||||||
|
|
||||||
@@ -234,7 +235,7 @@ export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralT
|
|||||||
strength: context.general.stats.strength + Math.round(context.general.stats.intelligence / 4),
|
strength: context.general.stats.strength + Math.round(context.general.stats.intelligence / 4),
|
||||||
intelligence: context.general.stats.intelligence + Math.round(context.general.stats.strength / 4),
|
intelligence: context.general.stats.intelligence + Math.round(context.general.stats.strength / 4),
|
||||||
};
|
};
|
||||||
const maxStatLevel = this.env.maxStatLevel ?? 255;
|
const maxStatLevel = this.env.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL;
|
||||||
const leadership = this.pipeline.onCalcStat(
|
const leadership = this.pipeline.onCalcStat(
|
||||||
context,
|
context,
|
||||||
'leadership',
|
'leadership',
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Ref GameConstBase::$maxLevel. Scenario `stat.max` is a separate join-time
|
||||||
|
// allocation rule and must not be used as this runtime fallback.
|
||||||
|
export const LEGACY_DEFAULT_MAX_LEVEL = 255;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './types.js';
|
export * from './types.js';
|
||||||
export * from './parseScenario.js';
|
export * from './parseScenario.js';
|
||||||
export * from './scenarioEffect.js';
|
export * from './scenarioEffect.js';
|
||||||
|
export * from './constants.js';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js'
|
|||||||
import { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js';
|
import { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js';
|
||||||
import { LogCategory, LogFormat, LogScope, type LogEntryDraft } from '@sammo-ts/logic/logging/types.js';
|
import { LogCategory, LogFormat, LogScope, type LogEntryDraft } from '@sammo-ts/logic/logging/types.js';
|
||||||
import { buildCrewTypeIndex, getTechCost, getTechLevel } from '@sammo-ts/logic/world/unitSet.js';
|
import { buildCrewTypeIndex, getTechCost, getTechLevel } from '@sammo-ts/logic/world/unitSet.js';
|
||||||
|
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic/scenario/constants.js';
|
||||||
import type { WarUnitReport } from './types.js';
|
import type { WarUnitReport } from './types.js';
|
||||||
import type {
|
import type {
|
||||||
ConquerCityOutcome,
|
ConquerCityOutcome,
|
||||||
@@ -27,7 +28,6 @@ import {
|
|||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|
||||||
const META_DEAD = 'dead';
|
const META_DEAD = 'dead';
|
||||||
const MAX_EXP_LEVEL = 255;
|
|
||||||
const MAX_DEDICATION_LEVEL = 30;
|
const MAX_DEDICATION_LEVEL = 30;
|
||||||
|
|
||||||
const updateLegacyProgressionLevels = (general: General): void => {
|
const updateLegacyProgressionLevels = (general: General): void => {
|
||||||
@@ -35,7 +35,7 @@ const updateLegacyProgressionLevels = (general: General): void => {
|
|||||||
general.experience < 1_000
|
general.experience < 1_000
|
||||||
? Math.trunc(general.experience / 100)
|
? Math.trunc(general.experience / 100)
|
||||||
: Math.trunc(Math.sqrt(general.experience / 10));
|
: Math.trunc(Math.sqrt(general.experience / 10));
|
||||||
general.meta.explevel = clamp(expLevel, 0, MAX_EXP_LEVEL);
|
general.meta.explevel = clamp(expLevel, 0, LEGACY_DEFAULT_MAX_LEVEL);
|
||||||
general.meta.dedlevel = clamp(Math.ceil(Math.sqrt(general.dedication) / 10), 0, MAX_DEDICATION_LEVEL);
|
general.meta.dedlevel = clamp(Math.ceil(Math.sqrt(general.dedication) / 10), 0, MAX_DEDICATION_LEVEL);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js';
|
|||||||
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
|
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
|
||||||
import type { WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
import type { WarStatName } from '@sammo-ts/logic/actionModules/types.js';
|
||||||
import { getTechAbility, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
|
import { getTechAbility, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
|
||||||
|
import { LEGACY_DEFAULT_MAX_LEVEL } from '@sammo-ts/logic/scenario/constants.js';
|
||||||
import type { WarActionPipeline, WarActionContext } from '../actions.js';
|
import type { WarActionPipeline, WarActionContext } from '../actions.js';
|
||||||
import type { WarEngineConfig } from '../types.js';
|
import type { WarEngineConfig } from '../types.js';
|
||||||
import type { WarCrewType } from '../crewType.js';
|
import type { WarCrewType } from '../crewType.js';
|
||||||
@@ -28,7 +29,6 @@ const META_RANK_PREFIX = 'rank_';
|
|||||||
const META_INTEL_EXP = 'intel_exp';
|
const META_INTEL_EXP = 'intel_exp';
|
||||||
const META_STRENGTH_EXP = 'strength_exp';
|
const META_STRENGTH_EXP = 'strength_exp';
|
||||||
const META_LEADERSHIP_EXP = 'leadership_exp';
|
const META_LEADERSHIP_EXP = 'leadership_exp';
|
||||||
const MAX_EXP_LEVEL = 255;
|
|
||||||
|
|
||||||
const RANK_WARNUM = `${META_RANK_PREFIX}warnum`;
|
const RANK_WARNUM = `${META_RANK_PREFIX}warnum`;
|
||||||
const RANK_KILLNUM = `${META_RANK_PREFIX}killnum`;
|
const RANK_KILLNUM = `${META_RANK_PREFIX}killnum`;
|
||||||
@@ -178,7 +178,7 @@ export class WarUnitGeneral<
|
|||||||
}) / 4
|
}) / 4
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const maxGeneralStat = this.config.maxGeneralStat ?? 255;
|
const maxGeneralStat = this.config.maxGeneralStat ?? LEGACY_DEFAULT_MAX_LEVEL;
|
||||||
value = clamp(value, 0, maxGeneralStat);
|
value = clamp(value, 0, maxGeneralStat);
|
||||||
if (withActions) {
|
if (withActions) {
|
||||||
value = this.actionPipeline.onCalcStat(this.getActionContext(), statName, value);
|
value = this.actionPipeline.onCalcStat(this.getActionContext(), statName, value);
|
||||||
@@ -360,7 +360,7 @@ export class WarUnitGeneral<
|
|||||||
this.general.experience < 1000
|
this.general.experience < 1000
|
||||||
? Math.trunc(this.general.experience / 100)
|
? Math.trunc(this.general.experience / 100)
|
||||||
: Math.trunc(Math.sqrt(this.general.experience / 10));
|
: Math.trunc(Math.sqrt(this.general.experience / 10));
|
||||||
const resolvedExpLevel = clamp(nextExpLevel, 0, MAX_EXP_LEVEL);
|
const resolvedExpLevel = clamp(nextExpLevel, 0, LEGACY_DEFAULT_MAX_LEVEL);
|
||||||
this.general.meta[META_EXP_LEVEL] = resolvedExpLevel;
|
this.general.meta[META_EXP_LEVEL] = resolvedExpLevel;
|
||||||
if (resolvedExpLevel === previousExpLevel) {
|
if (resolvedExpLevel === previousExpLevel) {
|
||||||
return;
|
return;
|
||||||
@@ -546,7 +546,7 @@ export class WarUnitGeneral<
|
|||||||
// one accumulated stat-exp threshold even though che_출병 itself does
|
// one accumulated stat-exp threshold even though che_출병 itself does
|
||||||
// not have the generic command progression tail.
|
// not have the generic command progression tail.
|
||||||
const limit = this.config.statUpgradeLimit ?? 30;
|
const limit = this.config.statUpgradeLimit ?? 30;
|
||||||
const maxStat = this.config.maxGeneralStat ?? 255;
|
const maxStat = this.config.maxGeneralStat ?? LEGACY_DEFAULT_MAX_LEVEL;
|
||||||
const entries = [
|
const entries = [
|
||||||
['leadership', META_LEADERSHIP_EXP, '통솔'],
|
['leadership', META_LEADERSHIP_EXP, '통솔'],
|
||||||
['strength', META_STRENGTH_EXP, '무력'],
|
['strength', META_STRENGTH_EXP, '무력'],
|
||||||
|
|||||||
Reference in New Issue
Block a user