refactor: enforce package boundaries
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { WorldStateRow } from '../context.js';
|
||||
import type { BattleSimJobPayload, BattleSimRequestPayload } from './types.js';
|
||||
import { loadUnitSetDefinitionByName } from './unitSetLoader.js';
|
||||
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
|
||||
import { normalizeScenarioEffect, type ScenarioEffectKey, type WarEngineConfig } from '@sammo-ts/logic';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import type { UnitSetDefinition } from '@sammo-ts/logic';
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { parseUnitSetDefinition, type UnitSetDefinition } from '@sammo-ts/logic';
|
||||
|
||||
import { resolveWorkspaceRoot } from '../paths.js';
|
||||
|
||||
const REPO_ROOT = resolveWorkspaceRoot();
|
||||
const DEFAULT_UNIT_SET_ROOT = path.resolve(REPO_ROOT, 'resources', 'unitset');
|
||||
|
||||
export interface UnitSetLoaderOptions {
|
||||
unitSetRoot?: string;
|
||||
filePrefix?: string;
|
||||
}
|
||||
|
||||
const readJsonFile = async (filePath: string): Promise<unknown> => {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
return JSON.parse(raw) as unknown;
|
||||
};
|
||||
|
||||
const resolveUnitSetRoot = (options?: UnitSetLoaderOptions): string => options?.unitSetRoot ?? DEFAULT_UNIT_SET_ROOT;
|
||||
|
||||
export const resolveUnitSetDefinitionPath = (unitSetName: string, options?: UnitSetLoaderOptions): string => {
|
||||
const prefix = options?.filePrefix ?? 'unitset_';
|
||||
return path.resolve(resolveUnitSetRoot(options), `${prefix}${unitSetName}.json`);
|
||||
};
|
||||
|
||||
export const loadUnitSetDefinition = async (unitSetPath: string): Promise<UnitSetDefinition> => {
|
||||
const raw = await readJsonFile(unitSetPath);
|
||||
return parseUnitSetDefinition(raw);
|
||||
};
|
||||
|
||||
export const loadUnitSetDefinitionByName = async (
|
||||
unitSetName: string,
|
||||
options?: UnitSetLoaderOptions
|
||||
): Promise<UnitSetDefinition> => {
|
||||
const unitSetPath = resolveUnitSetDefinitionPath(unitSetName, options);
|
||||
return loadUnitSetDefinition(unitSetPath);
|
||||
};
|
||||
@@ -1,25 +1,6 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { loadMapDefinitionByName as loadRuntimeMapDefinitionByName } from '@sammo-ts/game-engine/scenario/mapLoader.js';
|
||||
import type { MapDefinition } from '@sammo-ts/logic';
|
||||
|
||||
import { MapDefinitionSchema, type MapDefinition } from '@sammo-ts/logic';
|
||||
|
||||
const resolveWorkspaceRoot = (): string => {
|
||||
let current = path.resolve(process.cwd());
|
||||
for (let depth = 0; depth <= 6; depth += 1) {
|
||||
if (existsSync(path.join(current, 'pnpm-workspace.yaml'))) {
|
||||
return current;
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
return path.resolve(process.cwd());
|
||||
};
|
||||
|
||||
const RESOURCE_MAP_ROOT = path.resolve(resolveWorkspaceRoot(), 'resources/map');
|
||||
const mapCache = new Map<string, MapDefinition>();
|
||||
|
||||
export const loadMapDefinitionByName = async (mapName: string): Promise<MapDefinition> => {
|
||||
@@ -30,8 +11,7 @@ export const loadMapDefinitionByName = async (mapName: string): Promise<MapDefin
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const raw = await fs.readFile(path.join(RESOURCE_MAP_ROOT, `map_${mapName}.json`), 'utf-8');
|
||||
const map = MapDefinitionSchema.parse(JSON.parse(raw));
|
||||
const map = await loadRuntimeMapDefinitionByName(mapName);
|
||||
mapCache.set(mapName, map);
|
||||
return map;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { loadScenarioDefinitionById } from '@sammo-ts/game-engine/scenario/scenarioLoader.js';
|
||||
import type { ScenarioDefinition } from '@sammo-ts/logic';
|
||||
|
||||
import { loadMapDefinitionByName } from './mapDefinition.js';
|
||||
|
||||
export interface MapLayoutCity {
|
||||
@@ -19,311 +20,52 @@ export interface MapLayout {
|
||||
levelMap: Record<number, string>;
|
||||
}
|
||||
|
||||
interface ParsedCityConst {
|
||||
initCity?: unknown[];
|
||||
regionMap?: Record<string, unknown>;
|
||||
levelMap?: Record<string, unknown>;
|
||||
export interface MapLayoutLoaderOptions {
|
||||
loadScenario?: (scenarioId: number) => Promise<ScenarioDefinition>;
|
||||
loadMap?: typeof loadMapDefinitionByName;
|
||||
}
|
||||
|
||||
const LEGACY_SCENARIO_ROOT = path.resolve(process.cwd(), 'legacy/hwe/scenario');
|
||||
const LEGACY_MAP_ROOT = path.resolve(LEGACY_SCENARIO_ROOT, 'map');
|
||||
const LEGACY_CITY_CONST = path.resolve(process.cwd(), 'legacy/hwe/sammo/CityConstBase.php');
|
||||
|
||||
const layoutCache = new Map<string, MapLayout>();
|
||||
|
||||
const stripComments = (value: string): string => value.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
||||
|
||||
const extractPhpArray = (source: string, marker: string): string | null => {
|
||||
const idx = source.indexOf(marker);
|
||||
if (idx < 0) {
|
||||
const parseScenarioId = (scenario: string): number | null => {
|
||||
const normalized = scenario.replace(/^scenario_/i, '').replace(/\.json$/i, '');
|
||||
if (!/^\d+$/.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
const start = source.indexOf('[', idx);
|
||||
if (start < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let inString = false;
|
||||
let stringChar = '';
|
||||
|
||||
for (let i = start; i < source.length; i += 1) {
|
||||
const char = source[i];
|
||||
if (inString) {
|
||||
if (char === stringChar && source[i - 1] !== '\\') {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
inString = true;
|
||||
stringChar = char;
|
||||
continue;
|
||||
}
|
||||
if (char === '[') {
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
if (char === ']') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
return source.slice(start, i + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
const scenarioId = Number(normalized);
|
||||
return Number.isSafeInteger(scenarioId) ? scenarioId : null;
|
||||
};
|
||||
|
||||
const parsePhpArray = (input: string): unknown => {
|
||||
let index = 0;
|
||||
|
||||
const skipWhitespace = () => {
|
||||
while (index < input.length && /\s/.test(input[index] ?? '')) {
|
||||
index += 1;
|
||||
}
|
||||
};
|
||||
|
||||
const parseString = () => {
|
||||
const quote = input[index];
|
||||
index += 1;
|
||||
let value = '';
|
||||
while (index < input.length) {
|
||||
const char = input[index];
|
||||
if (char === quote && input[index - 1] !== '\\') {
|
||||
index += 1;
|
||||
return value;
|
||||
}
|
||||
value += char;
|
||||
index += 1;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const parseNumber = () => {
|
||||
let raw = '';
|
||||
while (index < input.length && /[0-9.+-]/.test(input[index] ?? '')) {
|
||||
raw += input[index];
|
||||
index += 1;
|
||||
}
|
||||
return Number(raw);
|
||||
};
|
||||
|
||||
const parseValue = (): unknown => {
|
||||
skipWhitespace();
|
||||
const char = input[index];
|
||||
if (!char) {
|
||||
return null;
|
||||
}
|
||||
if (char === '[') {
|
||||
return parseArray();
|
||||
}
|
||||
if (char === '"' || char === "'") {
|
||||
return parseString();
|
||||
}
|
||||
if (/[0-9.+-]/.test(char)) {
|
||||
return parseNumber();
|
||||
}
|
||||
if (input.startsWith('true', index)) {
|
||||
index += 4;
|
||||
return true;
|
||||
}
|
||||
if (input.startsWith('false', index)) {
|
||||
index += 5;
|
||||
return false;
|
||||
}
|
||||
if (input.startsWith('null', index)) {
|
||||
index += 4;
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseArray = (): unknown => {
|
||||
const output: unknown[] = [];
|
||||
const objectOutput: Record<string, unknown> = {};
|
||||
let hasKeyed = false;
|
||||
index += 1;
|
||||
|
||||
while (index < input.length) {
|
||||
skipWhitespace();
|
||||
if (input[index] === ']') {
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
const keyOrValue = parseValue();
|
||||
skipWhitespace();
|
||||
if (input.slice(index, index + 2) === '=>') {
|
||||
hasKeyed = true;
|
||||
index += 2;
|
||||
const value = parseValue();
|
||||
objectOutput[String(keyOrValue)] = value;
|
||||
} else if (keyOrValue !== null) {
|
||||
output.push(keyOrValue);
|
||||
}
|
||||
skipWhitespace();
|
||||
if (input[index] === ',') {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasKeyed) {
|
||||
return objectOutput;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
return parseValue();
|
||||
};
|
||||
|
||||
const parseCityConstFile = async (filePath: string): Promise<ParsedCityConst> => {
|
||||
const resolveMapName = async (
|
||||
scenario: string,
|
||||
loadScenario: (scenarioId: number) => Promise<ScenarioDefinition>
|
||||
): Promise<string> => {
|
||||
const scenarioId = parseScenarioId(scenario);
|
||||
if (scenarioId === null) {
|
||||
return 'che';
|
||||
}
|
||||
try {
|
||||
const raw = await fs.readFile(filePath, 'utf-8');
|
||||
const source = stripComments(raw);
|
||||
const initCityRaw = extractPhpArray(source, '$initCity');
|
||||
const regionMapRaw = extractPhpArray(source, '$regionMap');
|
||||
const levelMapRaw = extractPhpArray(source, '$levelMap');
|
||||
|
||||
return {
|
||||
initCity: initCityRaw ? (parsePhpArray(initCityRaw) as unknown[]) : undefined,
|
||||
regionMap: regionMapRaw ? (parsePhpArray(regionMapRaw) as Record<string, unknown>) : undefined,
|
||||
levelMap: levelMapRaw ? (parsePhpArray(levelMapRaw) as Record<string, unknown>) : undefined,
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const resolveScenarioFile = async (scenario: string): Promise<string> => {
|
||||
const normalized = scenario.replace(/\.json$/i, '');
|
||||
const candidates = [`${normalized}.json`, `scenario_${normalized}.json`, 'default.json'];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const fullPath = path.join(LEGACY_SCENARIO_ROOT, candidate);
|
||||
try {
|
||||
await fs.access(fullPath);
|
||||
return fullPath;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(LEGACY_SCENARIO_ROOT, 'default.json');
|
||||
};
|
||||
|
||||
const resolveMapName = async (scenario: string): Promise<string> => {
|
||||
const scenarioPath = await resolveScenarioFile(scenario);
|
||||
try {
|
||||
const raw = await fs.readFile(scenarioPath, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as { map?: { mapName?: string } };
|
||||
return parsed.map?.mapName ?? 'che';
|
||||
const definition = await loadScenario(scenarioId);
|
||||
return definition.config.environment.mapName;
|
||||
} catch {
|
||||
// 운영 DB가 보존된 상태에서 해당 commit에 scenario resource가 없을 수
|
||||
// 있으므로 기존 기본 map인 che로 안전하게 돌아갑니다.
|
||||
return 'che';
|
||||
}
|
||||
};
|
||||
|
||||
const buildLookupMap = (raw: Record<string, unknown> | undefined) => {
|
||||
const idToName: Record<number, string> = {};
|
||||
const nameToId: Record<string, number> = {};
|
||||
if (!raw) {
|
||||
return { idToName, nameToId };
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
const numericKey = Number(key);
|
||||
if (typeof value === 'string' && Number.isFinite(numericKey)) {
|
||||
idToName[numericKey] = value;
|
||||
continue;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
nameToId[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return { idToName, nameToId };
|
||||
};
|
||||
|
||||
const normalizeInitCity = (
|
||||
initCity: unknown[],
|
||||
levelMap: ReturnType<typeof buildLookupMap>,
|
||||
regionMap: ReturnType<typeof buildLookupMap>
|
||||
): MapLayoutCity[] => {
|
||||
const rows = initCity.filter(Array.isArray) as unknown[][];
|
||||
const nameToId = new Map<string, number>();
|
||||
|
||||
for (const row of rows) {
|
||||
if (typeof row[0] === 'number' && typeof row[1] === 'string') {
|
||||
nameToId.set(row[1], row[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
.map((row) => {
|
||||
const [id, name, levelLabel, _pop, _agri, _comm, _secu, _def, _wall, regionLabel, x, y, path] = row;
|
||||
if (typeof id !== 'number' || typeof name !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const levelValue =
|
||||
typeof levelLabel === 'number'
|
||||
? levelLabel
|
||||
: typeof levelLabel === 'string'
|
||||
? (levelMap.nameToId[levelLabel] ?? Number(levelLabel))
|
||||
: 0;
|
||||
|
||||
const regionValue =
|
||||
typeof regionLabel === 'number'
|
||||
? regionLabel
|
||||
: typeof regionLabel === 'string'
|
||||
? (regionMap.nameToId[regionLabel] ?? Number(regionLabel))
|
||||
: 0;
|
||||
|
||||
const pathNames = Array.isArray(path) ? (path as string[]) : [];
|
||||
const pathIds = pathNames
|
||||
.map((pathName) => nameToId.get(pathName))
|
||||
.filter((value): value is number => typeof value === 'number');
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
level: Number.isFinite(levelValue) ? levelValue : 0,
|
||||
region: Number.isFinite(regionValue) ? regionValue : 0,
|
||||
x: typeof x === 'number' ? x : 0,
|
||||
y: typeof y === 'number' ? y : 0,
|
||||
path: pathIds,
|
||||
} satisfies MapLayoutCity;
|
||||
})
|
||||
.filter((value): value is MapLayoutCity => value !== null);
|
||||
};
|
||||
|
||||
export const loadMapLayout = async (scenario: string): Promise<MapLayout> => {
|
||||
const mapName = await resolveMapName(scenario);
|
||||
const cached = layoutCache.get(mapName);
|
||||
export const loadMapLayout = async (scenario: string, options: MapLayoutLoaderOptions = {}): Promise<MapLayout> => {
|
||||
const mapName = await resolveMapName(scenario, options.loadScenario ?? loadScenarioDefinitionById);
|
||||
const useCache = !options.loadScenario && !options.loadMap;
|
||||
const cached = useCache ? layoutCache.get(mapName) : undefined;
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const base = await parseCityConstFile(LEGACY_CITY_CONST);
|
||||
const mapPath = path.join(LEGACY_MAP_ROOT, `${mapName}.php`);
|
||||
const map = await parseCityConstFile(mapPath);
|
||||
|
||||
const regionMapRaw = {
|
||||
...(base.regionMap ?? {}),
|
||||
...(map.regionMap ?? {}),
|
||||
};
|
||||
const levelMapRaw = {
|
||||
...(base.levelMap ?? {}),
|
||||
...(map.levelMap ?? {}),
|
||||
};
|
||||
|
||||
const regionMap = buildLookupMap(regionMapRaw);
|
||||
const levelMap = buildLookupMap(levelMapRaw);
|
||||
|
||||
const initCity = map.initCity ?? base.initCity ?? [];
|
||||
let cityList = normalizeInitCity(initCity, levelMap, regionMap);
|
||||
if (cityList.length === 0) {
|
||||
const resourceMap = await loadMapDefinitionByName(mapName);
|
||||
cityList = resourceMap.cities.map((city) => ({
|
||||
const map = await (options.loadMap ?? loadMapDefinitionByName)(mapName);
|
||||
const layout: MapLayout = {
|
||||
mapName,
|
||||
cityList: map.cities.map((city) => ({
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
level: city.level,
|
||||
@@ -331,16 +73,13 @@ export const loadMapLayout = async (scenario: string): Promise<MapLayout> => {
|
||||
x: city.position.x,
|
||||
y: city.position.y,
|
||||
path: [...city.connections],
|
||||
}));
|
||||
}
|
||||
|
||||
const layout: MapLayout = {
|
||||
mapName,
|
||||
cityList,
|
||||
regionMap: regionMap.idToName,
|
||||
levelMap: levelMap.idToName,
|
||||
})),
|
||||
regionMap: {},
|
||||
levelMap: {},
|
||||
};
|
||||
|
||||
layoutCache.set(mapName, layout);
|
||||
if (useCache) {
|
||||
layoutCache.set(mapName, layout);
|
||||
}
|
||||
return layout;
|
||||
};
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const hasWorkspaceMarker = (dir: string): boolean => fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'));
|
||||
|
||||
export const resolveWorkspaceRoot = (
|
||||
startDir: string = process.env.GAME_WORKSPACE_ROOT ?? process.env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(),
|
||||
maxDepth = 6
|
||||
): string => {
|
||||
let current = path.resolve(startDir);
|
||||
for (let depth = 0; depth <= maxDepth; depth += 1) {
|
||||
if (hasWorkspaceMarker(current)) return current;
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
return path.resolve(startDir);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/logic';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import type { GameApiContext } from '../../context.js';
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
ConflictingTurnDaemonCommandError,
|
||||
RejectedNpcPossessionCommandError,
|
||||
} from '../../daemon/databaseTransport.js';
|
||||
import { NpcPossessionError, reserveNpcPossessionCandidates } from '@sammo-ts/game-engine';
|
||||
import { NpcPossessionError, reserveNpcPossessionCandidates } from '@sammo-ts/game-engine/turn/npcPossessionService.js';
|
||||
import { resolveNationScoutMessage } from '../nation/shared.js';
|
||||
|
||||
const resolveSelectionCommandResult = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { LogCategory } from '@sammo-ts/infra';
|
||||
import { LogCategory } from '@sammo-ts/logic';
|
||||
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/logic';
|
||||
|
||||
import { authedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
import { getGoldIncome, getOutcome, getRiceIncome, getWallIncome, getWarGoldIncome } from '@sammo-ts/logic';
|
||||
import {
|
||||
getGoldIncome,
|
||||
getOutcome,
|
||||
getRiceIncome,
|
||||
getWallIncome,
|
||||
getWarGoldIncome,
|
||||
LogCategory,
|
||||
LogScope,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
import { authedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import { loadUnitSetDefinitionByName } from '../../../battleSim/unitSetLoader.js';
|
||||
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import { assertNationAccess, resolveNationPermission } from '../shared.js';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
import { findCrewTypeById, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
|
||||
|
||||
import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
|
||||
import { loadUnitSetDefinitionByName } from '../../battleSim/unitSetLoader.js';
|
||||
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
|
||||
import type { GameApiContext } from '../../context.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
import { resolveSecretPermission } from '../shared/secretPermission.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/logic';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { GameApiContext } from '../../context.js';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { accessAuthedProcedure, authedProcedure, procedure, router } from '../..
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
import { loadWorldMap } from '../../maps/worldMap.js';
|
||||
import { loadMapLayout } from '../../maps/mapLayout.js';
|
||||
import { loadUnitSetDefinitionByName } from '../../battleSim/unitSetLoader.js';
|
||||
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
|
||||
import { getMyGeneral, getOwnedGeneral } from '../shared/general.js';
|
||||
import { getGeneralDirectory, getNationDirectory } from './directory.js';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/logic';
|
||||
|
||||
import type { GameApiContext } from '../../context.js';
|
||||
import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js';
|
||||
|
||||
@@ -37,7 +37,7 @@ export class RemoteContentImageStore implements ContentImageUploadStore {
|
||||
'x-image-request-id': requestId,
|
||||
'x-image-signature': signature,
|
||||
},
|
||||
body: input.body,
|
||||
body: new Uint8Array(input.body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Image repository upload failed with HTTP ${response.status}.`);
|
||||
|
||||
@@ -9,4 +9,4 @@ export {
|
||||
type SelectPoolCandidateDto,
|
||||
type SelectPoolCandidateInfo,
|
||||
type SelectPoolReservationDto,
|
||||
} from '@sammo-ts/game-engine';
|
||||
} from '@sammo-ts/game-engine/turn/selectPoolService.js';
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { loadTurnCommandProfile } from './turnCommandProfile.js';
|
||||
import { loadTurnCommandProfile } from '@sammo-ts/game-engine/turn/turnCommandProfile.js';
|
||||
|
||||
export type TurnCommandOptionValue = string | number;
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { DEFAULT_TURN_COMMAND_PROFILE, parseTurnCommandProfile, type TurnCommandProfile } from '@sammo-ts/logic';
|
||||
|
||||
import { resolveWorkspaceRoot } from '../paths.js';
|
||||
|
||||
const REPO_ROOT = resolveWorkspaceRoot();
|
||||
const DEFAULT_PROFILE_PATH = path.resolve(REPO_ROOT, 'resources', 'turn-commands', 'default.json');
|
||||
|
||||
export interface TurnCommandProfileOptions {
|
||||
filePath?: string;
|
||||
}
|
||||
|
||||
const readCommandProfile = async (filePath: string): Promise<TurnCommandProfile> => {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
return parseTurnCommandProfile(JSON.parse(raw) as unknown);
|
||||
};
|
||||
|
||||
export const loadTurnCommandProfile = async (options?: TurnCommandProfileOptions): Promise<TurnCommandProfile> => {
|
||||
const filePath = options?.filePath ?? process.env.TURN_COMMANDS_PATH ?? DEFAULT_PROFILE_PATH;
|
||||
try {
|
||||
return await readCommandProfile(filePath);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return DEFAULT_TURN_COMMAND_PROFILE;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { GameSessionTokenPayload } from '@sammo-ts/common/auth/gameToken';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/logic';
|
||||
|
||||
import type { DatabaseClient, GameApiContext } from '../src/context.js';
|
||||
import { appRouter } from '../src/router.js';
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { loadMapLayout } from '../src/maps/mapLayout.js';
|
||||
|
||||
describe('map layout resource adapter', () => {
|
||||
it('resolves the scenario map through the shared runtime loaders', async () => {
|
||||
const loadScenario = vi.fn().mockResolvedValue({
|
||||
config: { environment: { mapName: 'custom-map' } },
|
||||
});
|
||||
const loadMap = vi.fn().mockResolvedValue({
|
||||
cities: [
|
||||
{
|
||||
id: 7,
|
||||
name: '테스트 도시',
|
||||
level: 3,
|
||||
region: 2,
|
||||
position: { x: 11, y: 13 },
|
||||
connections: [8],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(loadMapLayout('scenario_2601.json', { loadScenario, loadMap })).resolves.toEqual({
|
||||
mapName: 'custom-map',
|
||||
cityList: [
|
||||
{
|
||||
id: 7,
|
||||
name: '테스트 도시',
|
||||
level: 3,
|
||||
region: 2,
|
||||
x: 11,
|
||||
y: 13,
|
||||
path: [8],
|
||||
},
|
||||
],
|
||||
regionMap: {},
|
||||
levelMap: {},
|
||||
});
|
||||
expect(loadScenario).toHaveBeenCalledWith(2601);
|
||||
expect(loadMap).toHaveBeenCalledWith('custom-map');
|
||||
});
|
||||
|
||||
it('retains the che fallback for unknown preserved scenarios', async () => {
|
||||
const loadMap = vi.fn().mockResolvedValue({ cities: [] });
|
||||
|
||||
await expect(
|
||||
loadMapLayout('custom-runtime', {
|
||||
loadScenario: vi.fn(),
|
||||
loadMap,
|
||||
})
|
||||
).resolves.toMatchObject({ mapName: 'che' });
|
||||
expect(loadMap).toHaveBeenCalledWith('che');
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ const hasWorkspaceMarker = (dir: string): boolean =>
|
||||
WORKSPACE_MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)));
|
||||
|
||||
export const resolveWorkspaceRoot = (
|
||||
startDir: string = process.env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(),
|
||||
startDir: string = process.env.GAME_WORKSPACE_ROOT ?? process.env.GATEWAY_WORKSPACE_ROOT ?? process.cwd(),
|
||||
maxDepth = 6
|
||||
): string => {
|
||||
let current = path.resolve(startDir);
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import type { City, MapDefinition } from '@sammo-ts/logic';
|
||||
|
||||
const buildConnectionMap = (map: MapDefinition): Map<number, number[]> => {
|
||||
const result = new Map<number, number[]>();
|
||||
for (const city of map.cities) {
|
||||
result.set(city.id, city.connections ?? []);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const searchAllDistanceByCityList = (
|
||||
map: MapDefinition,
|
||||
cityIds: number[]
|
||||
): Record<number, Record<number, number>> => {
|
||||
if (cityIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const connectionMap = buildConnectionMap(map);
|
||||
const citySet = new Set(cityIds);
|
||||
const result: Record<number, Record<number, number>> = {};
|
||||
|
||||
for (const startId of citySet) {
|
||||
const distances: Record<number, number> = { [startId]: 0 };
|
||||
const queue: Array<[number, number]> = [[startId, 0]];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const [currentId, dist] = queue.shift()!;
|
||||
const connections = connectionMap.get(currentId) ?? [];
|
||||
for (const nextId of connections) {
|
||||
if (!citySet.has(nextId)) {
|
||||
continue;
|
||||
}
|
||||
if (distances[nextId] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
distances[nextId] = dist + 1;
|
||||
queue.push([nextId, dist + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
result[startId] = distances;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const searchAllDistanceByNationList = (
|
||||
map: MapDefinition,
|
||||
cities: City[],
|
||||
nationIds: number[],
|
||||
suppliedCityOnly: boolean
|
||||
): Record<number, Record<number, number>> => {
|
||||
if (nationIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const cityIds = cities
|
||||
.filter((city) => nationIds.includes(city.nationId))
|
||||
.filter((city) => !suppliedCityOnly || city.supplyState > 0)
|
||||
.map((city) => city.id);
|
||||
return searchAllDistanceByCityList(map, cityIds);
|
||||
};
|
||||
|
||||
export const isNeighbor = (
|
||||
map: MapDefinition,
|
||||
cities: City[],
|
||||
nationA: number,
|
||||
nationB: number,
|
||||
includeNoSupply = true
|
||||
): boolean => {
|
||||
if (nationA === nationB) {
|
||||
return false;
|
||||
}
|
||||
const connectionMap = buildConnectionMap(map);
|
||||
const nationACities = new Set(
|
||||
cities
|
||||
.filter((city) => city.nationId === nationA)
|
||||
.filter((city) => includeNoSupply || city.supplyState > 0)
|
||||
.map((city) => city.id)
|
||||
);
|
||||
|
||||
const nationBCities = cities
|
||||
.filter((city) => city.nationId === nationB)
|
||||
.filter((city) => includeNoSupply || city.supplyState > 0)
|
||||
.map((city) => city.id);
|
||||
|
||||
for (const cityId of nationBCities) {
|
||||
const connections = connectionMap.get(cityId) ?? [];
|
||||
for (const adjId of connections) {
|
||||
if (nationACities.has(adjId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
valueFit,
|
||||
withCanonicalArgumentAliases,
|
||||
} from '../aiUtils.js';
|
||||
import { searchAllDistanceByNationList } from '../distance.js';
|
||||
import { searchAllDistanceByNationList } from '@sammo-ts/logic/world/distance.js';
|
||||
import { generalActionHandlers } from '../generalAiGeneralActions.js';
|
||||
import { nationActionHandlers } from '../generalAiNationActions.js';
|
||||
import { resolveConstraintEnv, type ConstraintEnv } from './constraint.js';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GeneralAI } from '../core.js';
|
||||
import { GAME_TICKS_PER_TURN } from '@sammo-ts/common';
|
||||
import { calcCityDevRatio } from '../../aiUtils.js';
|
||||
import { searchAllDistanceByCityList } from '../../distance.js';
|
||||
import { searchAllDistanceByCityList } from '@sammo-ts/logic/world/distance.js';
|
||||
|
||||
export const do천도 = (ai: GeneralAI) => {
|
||||
if (!ai.nation || !ai.nation.capitalCityId) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GeneralAI } from '../core.js';
|
||||
import { asRecord, joinYearMonth, parseYearMonth, readMetaNumber } from '../../aiUtils.js';
|
||||
import { isNeighbor } from '../../distance.js';
|
||||
import { isNeighbor } from '@sammo-ts/logic/world/distance.js';
|
||||
import { resolveNationIncome } from './helpers.js';
|
||||
|
||||
const isTechLimited = (ai: GeneralAI, tech: number): boolean => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
loadActionModuleBundle,
|
||||
} from '@sammo-ts/logic';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import { createRuntimeTrace } from './runtimeTrace.js';
|
||||
|
||||
// legacy GameConstBase 기본값
|
||||
const DEFAULT_GENERAL_GOLD = 1000;
|
||||
@@ -80,6 +81,7 @@ export const buildCommandEnv = (config: ScenarioConfig, unitSet?: UnitSetDefinit
|
||||
const constValues = asRecord(config.const);
|
||||
|
||||
return {
|
||||
trace: createRuntimeTrace(),
|
||||
...(unitSet ? { unitSet } : {}),
|
||||
scenarioEffect: config.environment.scenarioEffect ?? null,
|
||||
develCost: resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0),
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TraceEvent, TracePort, TraceSubject } from '@sammo-ts/logic/ports/trace.js';
|
||||
|
||||
const parseIds = (value: string | undefined): Set<string> => new Set(value?.split(',') ?? []);
|
||||
|
||||
const intersects = (configured: ReadonlySet<string>, requested: readonly number[] | undefined): boolean =>
|
||||
requested?.some((id) => configured.has(String(id))) ?? false;
|
||||
|
||||
export const createRuntimeTrace = (
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
write: (line: string) => void = (line) => process.stdout.write(line)
|
||||
): TracePort => {
|
||||
const generalIds = parseIds(env.CORE_AI_TRACE_GENERAL_IDS);
|
||||
const nationIds = parseIds(env.CORE_AI_TRACE_NATION_IDS);
|
||||
const warTechNationIds = parseIds(env.CORE_WAR_TECH_TRACE_NATION_IDS);
|
||||
|
||||
return {
|
||||
isEnabled(event: TraceEvent, subject: TraceSubject = {}): boolean {
|
||||
switch (event) {
|
||||
case 'AI_ACTION_PATCH_TRACE':
|
||||
return intersects(generalIds, subject.generalIds) || intersects(nationIds, subject.nationIds);
|
||||
case 'AI_WAR_TRACE':
|
||||
return intersects(generalIds, subject.generalIds);
|
||||
case 'AI_WAR_FIXTURE_CORE':
|
||||
return env.CORE_BATTLE_FIXTURE_TRACE === '1';
|
||||
case 'WAR_TECH_TRACE':
|
||||
return intersects(warTechNationIds, subject.nationIds);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
write(event: TraceEvent, payload: unknown): void {
|
||||
write(`${event} ${JSON.stringify(payload)}\n`);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { asNumber, asRecord, JosaUtil, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
import { GamePrisma, LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
import { GamePrisma } from '@sammo-ts/infra';
|
||||
import {
|
||||
EventDomesticTraitLoader,
|
||||
isEventDomesticTraitKey,
|
||||
isPersonalityTraitKey,
|
||||
LogCategory,
|
||||
LogScope,
|
||||
PERSONALITY_TRAIT_KEYS,
|
||||
simpleSerialize,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { createRuntimeTrace } from '../src/turn/runtimeTrace.js';
|
||||
|
||||
describe('runtime trace adapter', () => {
|
||||
it('maps environment filters to domain trace subjects', () => {
|
||||
const trace = createRuntimeTrace({
|
||||
CORE_AI_TRACE_GENERAL_IDS: '3,7',
|
||||
CORE_AI_TRACE_NATION_IDS: '11',
|
||||
CORE_WAR_TECH_TRACE_NATION_IDS: '13',
|
||||
CORE_BATTLE_FIXTURE_TRACE: '1',
|
||||
});
|
||||
|
||||
expect(trace.isEnabled('AI_ACTION_PATCH_TRACE', { generalIds: [7] })).toBe(true);
|
||||
expect(trace.isEnabled('AI_ACTION_PATCH_TRACE', { nationIds: [11] })).toBe(true);
|
||||
expect(trace.isEnabled('AI_WAR_TRACE', { generalIds: [3] })).toBe(true);
|
||||
expect(trace.isEnabled('WAR_TECH_TRACE', { nationIds: [13] })).toBe(true);
|
||||
expect(trace.isEnabled('AI_WAR_FIXTURE_CORE')).toBe(true);
|
||||
expect(trace.isEnabled('AI_WAR_TRACE', { generalIds: [9] })).toBe(false);
|
||||
expect(
|
||||
createRuntimeTrace({ CORE_AI_TRACE_GENERAL_IDS: ' 7' }).isEnabled('AI_WAR_TRACE', { generalIds: [7] })
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves the legacy line protocol', () => {
|
||||
const write = vi.fn();
|
||||
const trace = createRuntimeTrace({}, write);
|
||||
|
||||
trace.write('AI_WAR_TRACE', { generalId: 7 });
|
||||
|
||||
expect(write).toHaveBeenCalledWith('AI_WAR_TRACE {"generalId":7}\n');
|
||||
});
|
||||
});
|
||||
@@ -27,8 +27,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sammo-ts/common": "workspace:*",
|
||||
"@sammo-ts/game-api": "workspace:*",
|
||||
"@sammo-ts/gateway-api": "workspace:*",
|
||||
"@sammo-ts/logic": "workspace:*",
|
||||
"@tiptap/extension-image": "^3.5.0",
|
||||
"@tiptap/extension-link": "^3.5.0",
|
||||
@@ -48,6 +46,10 @@
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sammo-ts/game-api": "workspace:*",
|
||||
"@sammo-ts/game-engine": "workspace:*",
|
||||
"@sammo-ts/gateway-api": "workspace:*",
|
||||
"@sammo-ts/infra": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"autoprefixer": "^10.4.23",
|
||||
|
||||
@@ -55,19 +55,14 @@ export class RemoteUserIconStore implements UserIconUploadStore {
|
||||
input.body
|
||||
),
|
||||
},
|
||||
body: input.body,
|
||||
body: new Uint8Array(input.body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Image repository upload failed with HTTP ${response.status}.`);
|
||||
}
|
||||
const picture = `users/core2026/${input.filename}`;
|
||||
const payload: unknown = await response.json();
|
||||
if (
|
||||
!payload ||
|
||||
typeof payload !== 'object' ||
|
||||
!('path' in payload) ||
|
||||
payload.path !== `icons/${picture}`
|
||||
) {
|
||||
if (!payload || typeof payload !== 'object' || !('path' in payload) || payload.path !== `icons/${picture}`) {
|
||||
throw new Error('Image repository returned an unexpected upload path.');
|
||||
}
|
||||
return { picture, publicUrl: `${this.publicBaseUrl.replace(/\/$/, '')}/${picture}` };
|
||||
|
||||
@@ -3,7 +3,7 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
||||
|
||||
import { type ScenarioInstallOptions } from '@sammo-ts/game-engine';
|
||||
import type { ScenarioInstallOptions } from '@sammo-ts/game-engine/scenario/scenarioSeeder.js';
|
||||
import {
|
||||
createGamePostgresConnector,
|
||||
createRedisConnector,
|
||||
@@ -1630,9 +1630,7 @@ export class GatewayOrchestrator implements GatewayOrchestratorHandle {
|
||||
}
|
||||
|
||||
private async clearTournamentRuntimeStateFromRedis(profileName: string): Promise<void> {
|
||||
const connector = createRedisConnector(
|
||||
resolveRedisConfigFromEnv(this.processConfig.baseEnv ?? process.env)
|
||||
);
|
||||
const connector = createRedisConnector(resolveRedisConfigFromEnv(this.processConfig.baseEnv ?? process.env));
|
||||
await connector.connect();
|
||||
try {
|
||||
await clearTournamentRuntimeKeys(connector.client, profileName);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import type { ScenarioInstallOptions } from '@sammo-ts/game-engine';
|
||||
import type { ScenarioInstallOptions } from '@sammo-ts/game-engine/scenario/scenarioSeeder.js';
|
||||
|
||||
import { seedProfileDatabase, type AdminSeedUser } from './seedProfileDatabase.js';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { seedScenarioToDatabase, type ScenarioInstallOptions } from '@sammo-ts/game-engine';
|
||||
import { seedScenarioToDatabase, type ScenarioInstallOptions } from '@sammo-ts/game-engine/scenario/scenarioSeeder.js';
|
||||
import type { GamePrisma } from '@sammo-ts/infra';
|
||||
import { GameClock, asRecord, type GameClockMode } from '@sammo-ts/common';
|
||||
|
||||
|
||||
@@ -3,11 +3,8 @@ import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import {
|
||||
composeScenarioResource,
|
||||
loadScenarioDefinitionById,
|
||||
resolveScenarioDefaultsPath,
|
||||
} from '@sammo-ts/game-engine';
|
||||
import { composeScenarioResource } from '@sammo-ts/game-engine/scenario/scenarioComposition.js';
|
||||
import { loadScenarioDefinitionById } from '@sammo-ts/game-engine/scenario/scenarioLoader.js';
|
||||
import { parseScenarioDefaults, parseScenarioDefinition, type ScenarioDefaults } from '@sammo-ts/logic';
|
||||
import { resolveWorkspaceRoot } from '../orchestrator/workspaceRoot.js';
|
||||
|
||||
@@ -40,14 +37,12 @@ const SCENARIO_ROOT = path.join('resources', 'scenario');
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const REPO_ROOT = resolveWorkspaceRoot(process.env.GATEWAY_WORKSPACE_ROOT ?? __dirname);
|
||||
const SCENARIO_RESOURCE_ROOT = path.resolve(REPO_ROOT, SCENARIO_ROOT);
|
||||
|
||||
const previewCache = new Map<string, { loadedAt: number; data: ScenarioPreview[] }>();
|
||||
const defaultsCache = new Map<string, ScenarioDefaults>();
|
||||
|
||||
const resolveScenarioRoot = (): string => {
|
||||
const defaultsPath = resolveScenarioDefaultsPath();
|
||||
return path.dirname(defaultsPath);
|
||||
};
|
||||
const resolveScenarioRoot = (): string => SCENARIO_RESOURCE_ROOT;
|
||||
|
||||
const runGit = (args: string[]): Promise<{ ok: boolean; output: string }> =>
|
||||
new Promise((resolve) => {
|
||||
@@ -210,7 +205,7 @@ const countGeneralsByNation = (
|
||||
};
|
||||
|
||||
const buildScenarioPreview = async (scenarioId: number): Promise<ScenarioPreview> => {
|
||||
const scenario = await loadScenarioDefinitionById(scenarioId);
|
||||
const scenario = await loadScenarioDefinitionById(scenarioId, { scenarioRoot: SCENARIO_RESOURCE_ROOT });
|
||||
const resolveNationId = buildNationIdResolver(scenario.nations);
|
||||
|
||||
const baseCounts = new Map(scenario.nations.map((nation) => [nation.id, 0]));
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sammo-ts/common": "workspace:*",
|
||||
"@sammo-ts/game-api": "workspace:*",
|
||||
"@sammo-ts/gateway-api": "workspace:*",
|
||||
"@sammo-ts/logic": "workspace:*",
|
||||
"@trpc/client": "^11.8.1",
|
||||
"@trpc/server": "^11.8.1",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
@@ -31,6 +28,11 @@
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sammo-ts/game-api": "workspace:*",
|
||||
"@sammo-ts/game-engine": "workspace:*",
|
||||
"@sammo-ts/gateway-api": "workspace:*",
|
||||
"@sammo-ts/infra": "workspace:*",
|
||||
"@sammo-ts/logic": "workspace:*",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"autoprefixer": "^10.4.23",
|
||||
|
||||
Reference in New Issue
Block a user