Refactor equipped items into persistent instances
This commit is contained in:
@@ -3,6 +3,7 @@ import type {
|
||||
Constraint,
|
||||
ConstraintContext,
|
||||
General,
|
||||
GeneralItemSlots,
|
||||
GeneralActionDefinition,
|
||||
GeneralTurnCommandSpec,
|
||||
Nation,
|
||||
@@ -13,6 +14,7 @@ import type {
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic';
|
||||
import { evaluateConstraints, loadGeneralTurnCommandSpecs, loadNationTurnCommandSpecs } from '@sammo-ts/logic';
|
||||
import { projectItemSlots, readItemInventoryFromMeta } from '@sammo-ts/logic/items/index.js';
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
|
||||
import type { CityRow, GeneralRow, NationRow, WorldStateRow } from '../context.js';
|
||||
@@ -220,48 +222,54 @@ const buildConstraintEnv = (worldState: WorldStateRow): Record<string, unknown>
|
||||
};
|
||||
};
|
||||
|
||||
const mapGeneralRow = (row: GeneralRow): General => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
nationId: row.nationId,
|
||||
cityId: row.cityId,
|
||||
troopId: row.troopId,
|
||||
stats: {
|
||||
leadership: row.leadership,
|
||||
strength: row.strength,
|
||||
intelligence: row.intel,
|
||||
},
|
||||
experience: row.experience,
|
||||
dedication: row.dedication,
|
||||
officerLevel: row.officerLevel,
|
||||
role: {
|
||||
personality: normalizeCode(row.personalCode),
|
||||
specialDomestic: normalizeCode(row.specialCode),
|
||||
specialWar: normalizeCode(row.special2Code),
|
||||
items: {
|
||||
horse: normalizeCode(row.horseCode),
|
||||
weapon: normalizeCode(row.weaponCode),
|
||||
book: normalizeCode(row.bookCode),
|
||||
item: normalizeCode(row.itemCode),
|
||||
const mapGeneralRow = (row: GeneralRow): General => {
|
||||
const legacySlots: GeneralItemSlots = {
|
||||
horse: normalizeCode(row.horseCode),
|
||||
weapon: normalizeCode(row.weaponCode),
|
||||
book: normalizeCode(row.bookCode),
|
||||
item: normalizeCode(row.itemCode),
|
||||
};
|
||||
const meta = ensureGeneralMeta(asTriggerRecord(row.meta), row.id);
|
||||
const itemInventory = readItemInventoryFromMeta(meta, legacySlots);
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
nationId: row.nationId,
|
||||
cityId: row.cityId,
|
||||
troopId: row.troopId,
|
||||
stats: {
|
||||
leadership: row.leadership,
|
||||
strength: row.strength,
|
||||
intelligence: row.intel,
|
||||
},
|
||||
},
|
||||
injury: row.injury,
|
||||
gold: row.gold,
|
||||
rice: row.rice,
|
||||
crew: row.crew,
|
||||
crewTypeId: row.crewTypeId,
|
||||
train: row.train,
|
||||
atmos: row.atmos,
|
||||
age: row.age,
|
||||
npcState: row.npcState,
|
||||
triggerState: {
|
||||
flags: {},
|
||||
counters: {},
|
||||
modifiers: {},
|
||||
meta: {},
|
||||
},
|
||||
meta: ensureGeneralMeta(asTriggerRecord(row.meta), row.id),
|
||||
});
|
||||
experience: row.experience,
|
||||
dedication: row.dedication,
|
||||
officerLevel: row.officerLevel,
|
||||
role: {
|
||||
personality: normalizeCode(row.personalCode),
|
||||
specialDomestic: normalizeCode(row.specialCode),
|
||||
specialWar: normalizeCode(row.special2Code),
|
||||
items: projectItemSlots(itemInventory),
|
||||
},
|
||||
injury: row.injury,
|
||||
gold: row.gold,
|
||||
rice: row.rice,
|
||||
crew: row.crew,
|
||||
crewTypeId: row.crewTypeId,
|
||||
train: row.train,
|
||||
atmos: row.atmos,
|
||||
age: row.age,
|
||||
npcState: row.npcState,
|
||||
triggerState: {
|
||||
flags: {},
|
||||
counters: {},
|
||||
modifiers: {},
|
||||
meta: {},
|
||||
},
|
||||
itemInventory,
|
||||
meta,
|
||||
};
|
||||
};
|
||||
|
||||
const mapCityRow = (row: CityRow): City => {
|
||||
const meta = asTriggerRecord(row.meta);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createGamePostgresConnector, GamePrisma } from '@sammo-ts/infra';
|
||||
import { ActionLogger, ItemLoader, LogFormat, UserLogger, isItemKey } from '@sammo-ts/logic';
|
||||
import { cloneItemInventory, ensureItemInventory, equipNewItem } from '@sammo-ts/logic/items/index.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
|
||||
import type { TurnDaemonCommandResult } from '../lifecycle/types.js';
|
||||
@@ -392,14 +393,17 @@ export const createAuctionFinalizer = async (options: {
|
||||
};
|
||||
}
|
||||
}
|
||||
const nextBidder = {
|
||||
...bidder,
|
||||
role: { ...bidder.role, items: { ...bidder.role.items } },
|
||||
itemInventory: cloneItemInventory(ensureItemInventory(bidder)),
|
||||
};
|
||||
equipNewItem(nextBidder, slot, itemKey, {
|
||||
...(itemModule.initialCharges === undefined ? {} : { charges: itemModule.initialCharges }),
|
||||
});
|
||||
world.updateGeneral(bidder.id, {
|
||||
role: {
|
||||
...bidder.role,
|
||||
items: {
|
||||
...bidder.role.items,
|
||||
[slot]: itemKey,
|
||||
},
|
||||
},
|
||||
role: nextBidder.role,
|
||||
itemInventory: nextBidder.itemInventory,
|
||||
});
|
||||
|
||||
const bidderLogger = new ActionLogger({ generalId: bidder.id, nationId: bidder.nationId });
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { TurnDaemonCommandResult, TurnDaemonHooks } from '../lifecycle/type
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
import type { InMemoryReservedTurnStore } from './reservedTurnStore.js';
|
||||
import { buildDiplomacyMeta } from '@sammo-ts/logic';
|
||||
import { ensureItemInventory, withSerializedItemInventory } from '@sammo-ts/logic/items/index.js';
|
||||
|
||||
export interface DatabaseTurnHooks {
|
||||
hooks: TurnDaemonHooks;
|
||||
@@ -137,7 +138,7 @@ const buildGeneralUpdate = (
|
||||
personalCode: toCode(general.role.personality),
|
||||
specialCode: toCode(general.role.specialDomestic),
|
||||
special2Code: toCode(general.role.specialWar),
|
||||
meta: asJson(general.meta),
|
||||
meta: asJson(withSerializedItemInventory(general.meta, ensureItemInventory(general))),
|
||||
turnTime: general.turnTime,
|
||||
recentWarTime: general.recentWarTime ?? null,
|
||||
});
|
||||
@@ -172,7 +173,7 @@ const buildGeneralCreate = (
|
||||
personalCode: toCode(general.role.personality),
|
||||
specialCode: toCode(general.role.specialDomestic),
|
||||
special2Code: toCode(general.role.specialWar),
|
||||
meta: asJson(general.meta),
|
||||
meta: asJson(withSerializedItemInventory(general.meta, ensureItemInventory(general))),
|
||||
turnTime: general.turnTime,
|
||||
recentWarTime: general.recentWarTime ?? null,
|
||||
});
|
||||
|
||||
@@ -146,6 +146,7 @@ export const buildReservedTurnDefinitions = async (options: {
|
||||
reqSecu: item.reqSecu,
|
||||
buyable: item.buyable,
|
||||
unique: item.unique,
|
||||
...(item.initialCharges === undefined ? {} : { initialCharges: item.initialCharges }),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
@@ -20,6 +20,12 @@ import {
|
||||
type ItemModule,
|
||||
type TriggerValue,
|
||||
} from '@sammo-ts/logic';
|
||||
import {
|
||||
cloneItemInventory,
|
||||
ensureItemInventory,
|
||||
equipNewItem,
|
||||
removeEquippedItem,
|
||||
} from '@sammo-ts/logic/items/index.js';
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
import type { TurnGeneral } from './types.js';
|
||||
|
||||
@@ -622,21 +628,22 @@ async function handleDropItem(
|
||||
if (!general) {
|
||||
return { type: 'dropItem', ok: false, generalId: command.generalId, reason: '장수 정보를 찾을 수 없습니다.' };
|
||||
}
|
||||
const { itemType } = command;
|
||||
const items = { ...general.role.items };
|
||||
if (items.horse === itemType) items.horse = null;
|
||||
else if (items.weapon === itemType) items.weapon = null;
|
||||
else if (items.book === itemType) items.book = null;
|
||||
else if (items.item === itemType) items.item = null;
|
||||
else {
|
||||
const slot = (['horse', 'weapon', 'book', 'item'] as const).find(
|
||||
(candidate) => general.role.items[candidate] === command.itemType
|
||||
);
|
||||
if (!slot) {
|
||||
return { type: 'dropItem', ok: false, generalId: command.generalId, reason: '아이템을 가지고 있지 않습니다.' };
|
||||
}
|
||||
const nextGeneral = {
|
||||
...general,
|
||||
role: { ...general.role, items: { ...general.role.items } },
|
||||
itemInventory: cloneItemInventory(ensureItemInventory(general)),
|
||||
};
|
||||
removeEquippedItem(nextGeneral, slot);
|
||||
|
||||
world.updateGeneral(command.generalId, {
|
||||
role: {
|
||||
...general.role,
|
||||
items,
|
||||
},
|
||||
role: nextGeneral.role,
|
||||
itemInventory: nextGeneral.itemInventory,
|
||||
});
|
||||
return { type: 'dropItem', ok: true, generalId: command.generalId };
|
||||
}
|
||||
@@ -1068,13 +1075,16 @@ async function handleVoteReward(
|
||||
reason: '유니크 아이템을 찾을 수 없습니다.',
|
||||
};
|
||||
}
|
||||
patch.role = {
|
||||
...general.role,
|
||||
items: {
|
||||
...general.role.items,
|
||||
[itemModule.slot]: itemKey,
|
||||
},
|
||||
const nextGeneral = {
|
||||
...general,
|
||||
role: { ...general.role, items: { ...general.role.items } },
|
||||
itemInventory: cloneItemInventory(ensureItemInventory(general)),
|
||||
};
|
||||
equipNewItem(nextGeneral, itemModule.slot, itemKey, {
|
||||
...(itemModule.initialCharges === undefined ? {} : { charges: itemModule.initialCharges }),
|
||||
});
|
||||
patch.role = nextGeneral.role;
|
||||
patch.itemInventory = nextGeneral.itemInventory;
|
||||
|
||||
const nationName = world.getNationById(general.nationId)?.name ?? '재야';
|
||||
const generalName = general.name;
|
||||
|
||||
@@ -8,7 +8,16 @@ import {
|
||||
type TurnEngineNationRow,
|
||||
type TurnEngineTroopRow,
|
||||
} from '@sammo-ts/infra';
|
||||
import type { City, Nation, ScenarioConfig, ScenarioMeta, Troop, TriggerValue } from '@sammo-ts/logic';
|
||||
import type {
|
||||
City,
|
||||
GeneralItemSlots,
|
||||
Nation,
|
||||
ScenarioConfig,
|
||||
ScenarioMeta,
|
||||
Troop,
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic';
|
||||
import { projectItemSlots, readItemInventoryFromMeta } from '@sammo-ts/logic/items/index.js';
|
||||
import { z } from 'zod';
|
||||
import { asRecord, isRecord } from '@sammo-ts/common';
|
||||
|
||||
@@ -133,58 +142,64 @@ const mapScenarioConfig = (raw: JsonValue): ScenarioConfig => {
|
||||
return parsed.data;
|
||||
};
|
||||
|
||||
const mapGeneralRow = (row: TurnEngineGeneralRow): TurnGeneral => ({
|
||||
...((): { meta: TurnGeneral['meta'] } => {
|
||||
const meta = asTriggerRecord(row.meta) as Record<string, unknown>;
|
||||
const killturn = readMetaNumber(meta, 'killturn');
|
||||
if (killturn === null) {
|
||||
throw new Error(`general.meta.killturn is required (generalId=${row.id}).`);
|
||||
}
|
||||
return { meta: { ...meta, killturn } as TurnGeneral['meta'] };
|
||||
})(),
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
nationId: row.nationId,
|
||||
cityId: row.cityId,
|
||||
troopId: row.troopId,
|
||||
stats: {
|
||||
leadership: row.leadership,
|
||||
strength: row.strength,
|
||||
intelligence: row.intel,
|
||||
},
|
||||
experience: row.experience,
|
||||
dedication: row.dedication,
|
||||
officerLevel: row.officerLevel,
|
||||
role: {
|
||||
personality: normalizeCode(row.personalCode),
|
||||
specialDomestic: normalizeCode(row.specialCode),
|
||||
specialWar: normalizeCode(row.special2Code),
|
||||
items: {
|
||||
horse: normalizeCode(row.horseCode),
|
||||
weapon: normalizeCode(row.weaponCode),
|
||||
book: normalizeCode(row.bookCode),
|
||||
item: normalizeCode(row.itemCode),
|
||||
const mapGeneralRow = (row: TurnEngineGeneralRow): TurnGeneral => {
|
||||
const legacySlots: GeneralItemSlots = {
|
||||
horse: normalizeCode(row.horseCode),
|
||||
weapon: normalizeCode(row.weaponCode),
|
||||
book: normalizeCode(row.bookCode),
|
||||
item: normalizeCode(row.itemCode),
|
||||
};
|
||||
const rawMeta = asTriggerRecord(row.meta) as Record<string, unknown>;
|
||||
const itemInventory = readItemInventoryFromMeta(rawMeta, legacySlots);
|
||||
return {
|
||||
...((): { meta: TurnGeneral['meta'] } => {
|
||||
const meta = rawMeta;
|
||||
const killturn = readMetaNumber(meta, 'killturn');
|
||||
if (killturn === null) {
|
||||
throw new Error(`general.meta.killturn is required (generalId=${row.id}).`);
|
||||
}
|
||||
return { meta: { ...meta, killturn } as TurnGeneral['meta'] };
|
||||
})(),
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
nationId: row.nationId,
|
||||
cityId: row.cityId,
|
||||
troopId: row.troopId,
|
||||
stats: {
|
||||
leadership: row.leadership,
|
||||
strength: row.strength,
|
||||
intelligence: row.intel,
|
||||
},
|
||||
},
|
||||
injury: row.injury,
|
||||
gold: row.gold,
|
||||
rice: row.rice,
|
||||
crew: row.crew,
|
||||
crewTypeId: row.crewTypeId,
|
||||
train: row.train,
|
||||
atmos: row.atmos,
|
||||
age: row.age,
|
||||
npcState: row.npcState,
|
||||
triggerState: {
|
||||
flags: {},
|
||||
counters: {},
|
||||
modifiers: {},
|
||||
meta: {},
|
||||
},
|
||||
// meta는 상단에서 보장 처리됨.
|
||||
turnTime: row.turnTime,
|
||||
recentWarTime: row.recentWarTime ?? null,
|
||||
});
|
||||
experience: row.experience,
|
||||
dedication: row.dedication,
|
||||
officerLevel: row.officerLevel,
|
||||
role: {
|
||||
personality: normalizeCode(row.personalCode),
|
||||
specialDomestic: normalizeCode(row.specialCode),
|
||||
specialWar: normalizeCode(row.special2Code),
|
||||
items: projectItemSlots(itemInventory),
|
||||
},
|
||||
injury: row.injury,
|
||||
gold: row.gold,
|
||||
rice: row.rice,
|
||||
crew: row.crew,
|
||||
crewTypeId: row.crewTypeId,
|
||||
train: row.train,
|
||||
atmos: row.atmos,
|
||||
age: row.age,
|
||||
npcState: row.npcState,
|
||||
triggerState: {
|
||||
flags: {},
|
||||
counters: {},
|
||||
modifiers: {},
|
||||
meta: {},
|
||||
},
|
||||
itemInventory,
|
||||
// meta는 상단에서 보장 처리됨.
|
||||
turnTime: row.turnTime,
|
||||
recentWarTime: row.recentWarTime ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
const mapCityRow = (row: TurnEngineCityRow): City => {
|
||||
const meta = asTriggerRecord(row.meta);
|
||||
|
||||
Reference in New Issue
Block a user