merge: 최신 메인 변경 통합
This commit is contained in:
@@ -35,7 +35,11 @@ import {
|
||||
} from '../../turns/reservedTurns.js';
|
||||
import { getOwnedGeneral } from '../shared/general.js';
|
||||
import type { GameApiContext, GeneralRow, WorldStateRow } from '../../context.js';
|
||||
import { buildRefGeneralTargetOptions } from '../../turns/commandTargets.js';
|
||||
import {
|
||||
buildRefAmountPresets,
|
||||
buildRefGeneralTargetOptions,
|
||||
buildRefNationTargetOptions,
|
||||
} from '../../turns/commandTargets.js';
|
||||
|
||||
const zPushAmount = z
|
||||
.number()
|
||||
@@ -159,52 +163,125 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
|
||||
const moduleBundlePromise = environmentPromise.then((environment) =>
|
||||
loadActionModuleBundle(environment.unitSet, environment.scenarioEffect)
|
||||
);
|
||||
const [city, nation, nationGenerals, cities, nations, generals, environment, traits, moduleBundle, map] =
|
||||
await Promise.all([
|
||||
general.cityId > 0
|
||||
? ctx.db.city.findUnique({
|
||||
where: { id: general.cityId },
|
||||
})
|
||||
: null,
|
||||
general.nationId > 0
|
||||
? ctx.db.nation.findUnique({
|
||||
where: { id: general.nationId },
|
||||
})
|
||||
: null,
|
||||
general.nationId > 0
|
||||
? ctx.db.general.findMany({
|
||||
where: { nationId: general.nationId },
|
||||
})
|
||||
: Promise.resolve(null),
|
||||
ctx.db.city.findMany({ orderBy: { id: 'asc' } }),
|
||||
ctx.db.nation.findMany({
|
||||
select: { id: true, name: true, color: true },
|
||||
orderBy: { id: 'asc' },
|
||||
}),
|
||||
ctx.db.general.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
nationId: true,
|
||||
cityId: true,
|
||||
npcState: true,
|
||||
officerLevel: true,
|
||||
},
|
||||
orderBy: [{ npcState: 'asc' }, { name: 'asc' }, { id: 'asc' }],
|
||||
}),
|
||||
environmentPromise,
|
||||
loadBattleSimTraitOptions(),
|
||||
moduleBundlePromise,
|
||||
loadMapDefinitionByName(resolveMapName(worldState, ctx.profile.id)),
|
||||
]);
|
||||
const [
|
||||
city,
|
||||
nation,
|
||||
nationGenerals,
|
||||
cities,
|
||||
nations,
|
||||
generals,
|
||||
diplomacy,
|
||||
troops,
|
||||
environment,
|
||||
traits,
|
||||
moduleBundle,
|
||||
map,
|
||||
] = await Promise.all([
|
||||
general.cityId > 0
|
||||
? ctx.db.city.findUnique({
|
||||
where: { id: general.cityId },
|
||||
})
|
||||
: null,
|
||||
general.nationId > 0
|
||||
? ctx.db.nation.findUnique({
|
||||
where: { id: general.nationId },
|
||||
})
|
||||
: null,
|
||||
general.nationId > 0
|
||||
? ctx.db.general.findMany({
|
||||
where: { nationId: general.nationId },
|
||||
})
|
||||
: Promise.resolve(null),
|
||||
ctx.db.city.findMany({ orderBy: { id: 'asc' } }),
|
||||
ctx.db.nation.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
color: true,
|
||||
capitalCityId: true,
|
||||
level: true,
|
||||
meta: true,
|
||||
},
|
||||
orderBy: { id: 'asc' },
|
||||
}),
|
||||
ctx.db.general.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
nationId: true,
|
||||
cityId: true,
|
||||
npcState: true,
|
||||
officerLevel: true,
|
||||
gold: true,
|
||||
rice: true,
|
||||
crew: true,
|
||||
train: true,
|
||||
atmos: true,
|
||||
troopId: true,
|
||||
},
|
||||
orderBy: [{ npcState: 'asc' }, { name: 'asc' }, { id: 'asc' }],
|
||||
}),
|
||||
general.nationId > 0
|
||||
? ctx.db.diplomacy.findMany({ where: { srcNationId: general.nationId } })
|
||||
: Promise.resolve([]),
|
||||
general.nationId > 0
|
||||
? ctx.db.troop.findMany({ where: { nationId: general.nationId }, orderBy: { troopLeaderId: 'asc' } })
|
||||
: Promise.resolve([]),
|
||||
environmentPromise,
|
||||
loadBattleSimTraitOptions(),
|
||||
moduleBundlePromise,
|
||||
loadMapDefinitionByName(resolveMapName(worldState, ctx.profile.id)),
|
||||
]);
|
||||
|
||||
const nationById = new Map(nations.map((entry) => [entry.id, entry]));
|
||||
const cityById = new Map(cities.map((entry) => [entry.id, entry]));
|
||||
const generalCountByNation = new Map<number, number>();
|
||||
for (const entry of generals) {
|
||||
generalCountByNation.set(entry.nationId, (generalCountByNation.get(entry.nationId) ?? 0) + 1);
|
||||
}
|
||||
const cityCountByNation = new Map<number, number>();
|
||||
for (const entry of cities) {
|
||||
cityCountByNation.set(entry.nationId, (cityCountByNation.get(entry.nationId) ?? 0) + 1);
|
||||
}
|
||||
const diplomacyByNation = new Map(diplomacy.map((entry) => [entry.destNationId, entry]));
|
||||
const adjacentNationIds = new Set<number>();
|
||||
const actorCityIds = new Set(
|
||||
cities.filter((entry) => entry.nationId === general.nationId).map((entry) => entry.id)
|
||||
);
|
||||
for (const mapCity of map.cities) {
|
||||
if (!actorCityIds.has(mapCity.id)) continue;
|
||||
for (const adjacentId of mapCity.connections) {
|
||||
const adjacentNationId = cityById.get(adjacentId)?.nationId;
|
||||
if (adjacentNationId && adjacentNationId !== general.nationId) adjacentNationIds.add(adjacentNationId);
|
||||
}
|
||||
}
|
||||
const nationTargetOptions = buildRefNationTargetOptions({
|
||||
actorNationId: general.nationId,
|
||||
nations: nations.map((entry) => {
|
||||
const relation = diplomacyByNation.get(entry.id);
|
||||
return {
|
||||
id: entry.id,
|
||||
name: entry.name,
|
||||
color: entry.color,
|
||||
capitalName: entry.capitalCityId ? (cityById.get(entry.capitalCityId)?.name ?? '-') : '-',
|
||||
level: entry.level,
|
||||
power: readGeneralMetaNumber(entry.meta, 'power') ?? 0,
|
||||
generalCount: generalCountByNation.get(entry.id) ?? 0,
|
||||
cityCount: cityCountByNation.get(entry.id) ?? 0,
|
||||
diplomacyState: relation?.stateCode ?? 2,
|
||||
diplomacyTerm: relation?.term ?? 0,
|
||||
adjacent: adjacentNationIds.has(entry.id),
|
||||
diplomacyRestricted: (readGeneralMetaNumber(entry.meta, 'surlimit') ?? 0) !== 0,
|
||||
};
|
||||
}),
|
||||
});
|
||||
const generalTargetOptions = buildRefGeneralTargetOptions({
|
||||
actorId: general.id,
|
||||
actorNationId: general.nationId,
|
||||
generals,
|
||||
nationNames: new Map(nations.map((entry) => [entry.id, entry.name])),
|
||||
cityNames: new Map(cities.map((entry) => [entry.id, entry.name])),
|
||||
troopNames: new Map(troops.map((entry) => [entry.troopLeaderId, entry.name])),
|
||||
});
|
||||
const items: TurnCommandInputOptions['items'] = {
|
||||
horse: [{ value: 'None', label: '판매/해제' }],
|
||||
@@ -234,11 +311,8 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
|
||||
value: entry.id,
|
||||
label: `${entry.name} (${nationById.get(entry.nationId)?.name ?? '무주'})`,
|
||||
})),
|
||||
nations: nations.map((entry) => ({
|
||||
value: entry.id,
|
||||
label: entry.name,
|
||||
color: entry.color,
|
||||
})),
|
||||
nations: nationTargetOptions.nations,
|
||||
nationTargets: nationTargetOptions.nationTargets,
|
||||
generals: generalTargetOptions.generals,
|
||||
generalTargets: generalTargetOptions.generalTargets,
|
||||
crewTypes: (environment.unitSet.crewTypes ?? [])
|
||||
@@ -273,6 +347,10 @@ export const getTurnCommandTable = async (ctx: GameApiContext, generalId: number
|
||||
unitSet: environment.unitSet,
|
||||
generalActionModules: moduleBundle.general,
|
||||
}),
|
||||
amountPresets: buildRefAmountPresets(
|
||||
nation?.level ?? 0,
|
||||
readGeneralMetaNumber(asRecord(asRecord(worldState.config).const), 'maxResourceActionAmount') ?? 10_000
|
||||
),
|
||||
context: {
|
||||
actorGold: general.gold,
|
||||
actorRice: general.rice,
|
||||
|
||||
@@ -16,6 +16,19 @@ export interface TurnCommandOption {
|
||||
label: string;
|
||||
color?: string;
|
||||
description?: string;
|
||||
availableNow?: boolean;
|
||||
gold?: number;
|
||||
rice?: number;
|
||||
crew?: number;
|
||||
troopId?: number;
|
||||
}
|
||||
|
||||
export interface TurnCommandAmountPreset {
|
||||
values: number[];
|
||||
defaultValue: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
export interface TurnCommandRecruitmentCrewType {
|
||||
@@ -70,6 +83,7 @@ export interface TurnCommandInputField {
|
||||
export interface TurnCommandInputOptions {
|
||||
cities: TurnCommandOption[];
|
||||
nations: TurnCommandOption[];
|
||||
nationTargets?: Record<string, TurnCommandOption[]>;
|
||||
generals: TurnCommandOption[];
|
||||
generalTargets?: Record<string, TurnCommandOption[]>;
|
||||
crewTypes: TurnCommandOption[];
|
||||
@@ -78,6 +92,7 @@ export interface TurnCommandInputOptions {
|
||||
colors: TurnCommandOption[];
|
||||
items: Record<string, TurnCommandOption[]>;
|
||||
recruitment: TurnCommandRecruitmentInfo | null;
|
||||
amountPresets?: Record<string, TurnCommandAmountPreset>;
|
||||
context?: {
|
||||
actorGold: number;
|
||||
actorRice: number;
|
||||
|
||||
@@ -770,6 +770,7 @@ export const buildTurnCommandTable = async (options: {
|
||||
inputOptions: options.inputOptions ?? {
|
||||
cities: [],
|
||||
nations: [],
|
||||
nationTargets: {},
|
||||
generals: [],
|
||||
generalTargets: {},
|
||||
crewTypes: [],
|
||||
@@ -778,6 +779,7 @@ export const buildTurnCommandTable = async (options: {
|
||||
colors: [],
|
||||
items: {},
|
||||
recruitment: null,
|
||||
amountPresets: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { TurnCommandOption } from './commandInput.js';
|
||||
import { DIPLOMACY_STATE } from '@sammo-ts/logic';
|
||||
|
||||
import type { TurnCommandAmountPreset, TurnCommandOption } from './commandInput.js';
|
||||
|
||||
export interface GeneralTargetSource {
|
||||
id: number;
|
||||
@@ -7,6 +9,27 @@ export interface GeneralTargetSource {
|
||||
cityId: number;
|
||||
npcState: number;
|
||||
officerLevel: number;
|
||||
gold?: number;
|
||||
rice?: number;
|
||||
crew?: number;
|
||||
train?: number;
|
||||
atmos?: number;
|
||||
troopId?: number;
|
||||
}
|
||||
|
||||
export interface NationTargetSource {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
capitalName: string;
|
||||
level: number;
|
||||
power: number;
|
||||
generalCount: number;
|
||||
cityCount: number;
|
||||
diplomacyState: number;
|
||||
diplomacyTerm: number;
|
||||
adjacent: boolean;
|
||||
diplomacyRestricted?: boolean;
|
||||
}
|
||||
|
||||
export interface RefGeneralTargetOptions {
|
||||
@@ -14,6 +37,11 @@ export interface RefGeneralTargetOptions {
|
||||
generalTargets: Record<string, TurnCommandOption[]>;
|
||||
}
|
||||
|
||||
export interface RefNationTargetOptions {
|
||||
nations: TurnCommandOption[];
|
||||
nationTargets: Record<string, TurnCommandOption[]>;
|
||||
}
|
||||
|
||||
const SAME_NATION_GENERAL_COMMANDS = ['che_증여'] as const;
|
||||
const SAME_NATION_NATION_COMMANDS = ['che_발령', 'che_포상', 'che_몰수', 'che_부대탈퇴지시'] as const;
|
||||
|
||||
@@ -24,20 +52,54 @@ export const buildRefGeneralTargetOptions = (options: {
|
||||
generals: readonly GeneralTargetSource[];
|
||||
nationNames: ReadonlyMap<number, string>;
|
||||
cityNames: ReadonlyMap<number, string>;
|
||||
troopNames?: ReadonlyMap<number, string>;
|
||||
}): RefGeneralTargetOptions => {
|
||||
const toOption = (entry: GeneralTargetSource): TurnCommandOption => ({
|
||||
value: entry.id,
|
||||
label: `${entry.name} (${options.nationNames.get(entry.nationId) ?? '무소속'} · ${
|
||||
options.cityNames.get(entry.cityId) ?? '재야'
|
||||
})`,
|
||||
});
|
||||
const toOption = (entry: GeneralTargetSource, action?: string): TurnCommandOption => {
|
||||
const troopName = entry.troopId ? options.troopNames?.get(entry.troopId) : undefined;
|
||||
const isTroopMember = Boolean(entry.troopId && entry.troopId !== entry.id);
|
||||
const isTroopExit = action === 'che_부대탈퇴지시';
|
||||
const availableNow = isTroopExit ? isTroopMember && entry.id !== options.actorId : undefined;
|
||||
const details = [
|
||||
entry.gold === undefined ? null : `금 ${entry.gold.toLocaleString()}`,
|
||||
entry.rice === undefined ? null : `쌀 ${entry.rice.toLocaleString()}`,
|
||||
entry.crew === undefined ? null : `병력 ${entry.crew.toLocaleString()}`,
|
||||
entry.train === undefined ? null : `훈련 ${entry.train.toLocaleString()}`,
|
||||
entry.atmos === undefined ? null : `사기 ${entry.atmos.toLocaleString()}`,
|
||||
entry.troopId
|
||||
? `탑승 부대 ${troopName ?? `#${entry.troopId}`}${entry.troopId === entry.id ? ' (부대장)' : ''}`
|
||||
: '탑승 부대 없음',
|
||||
].filter((value): value is string => Boolean(value));
|
||||
if (isTroopExit) {
|
||||
details.unshift(availableNow ? '현재 탈퇴 지시 가능' : '현재 탈퇴 지시 불가');
|
||||
}
|
||||
return {
|
||||
value: entry.id,
|
||||
label: `${entry.name} (${options.nationNames.get(entry.nationId) ?? '무소속'} · ${
|
||||
options.cityNames.get(entry.cityId) ?? '재야'
|
||||
})`,
|
||||
description: details.join(' · '),
|
||||
...(availableNow === undefined ? {} : { availableNow }),
|
||||
...(entry.gold === undefined ? {} : { gold: entry.gold }),
|
||||
...(entry.rice === undefined ? {} : { rice: entry.rice }),
|
||||
...(entry.crew === undefined ? {} : { crew: entry.crew }),
|
||||
...(entry.troopId === undefined ? {} : { troopId: entry.troopId }),
|
||||
};
|
||||
};
|
||||
const project = (predicate: (entry: GeneralTargetSource) => boolean): TurnCommandOption[] =>
|
||||
options.generals.filter(predicate).map(toOption);
|
||||
options.generals.filter(predicate).map((entry) => toOption(entry));
|
||||
|
||||
const sameNation = project((entry) => entry.nationId === options.actorNationId);
|
||||
const generalTargets: Record<string, TurnCommandOption[]> = {};
|
||||
for (const action of SAME_NATION_GENERAL_COMMANDS) generalTargets[action] = sameNation;
|
||||
for (const action of SAME_NATION_NATION_COMMANDS) generalTargets[action] = sameNation;
|
||||
for (const action of SAME_NATION_GENERAL_COMMANDS) {
|
||||
generalTargets[action] = options.generals
|
||||
.filter((entry) => entry.nationId === options.actorNationId)
|
||||
.map((entry) => toOption(entry, action));
|
||||
}
|
||||
for (const action of SAME_NATION_NATION_COMMANDS) {
|
||||
generalTargets[action] = options.generals
|
||||
.filter((entry) => entry.nationId === options.actorNationId)
|
||||
.map((entry) => toOption(entry, action))
|
||||
.sort((left, right) => Number(right.availableNow) - Number(left.availableNow));
|
||||
}
|
||||
|
||||
generalTargets.che_선양 = project(
|
||||
(entry) => entry.nationId !== 0 && entry.nationId === options.actorNationId && entry.id !== options.actorId
|
||||
@@ -53,3 +115,124 @@ export const buildRefGeneralTargetOptions = (options: {
|
||||
generalTargets,
|
||||
};
|
||||
};
|
||||
|
||||
const DIPLOMACY_LABELS: Record<number, string> = {
|
||||
[DIPLOMACY_STATE.WAR]: '전쟁',
|
||||
[DIPLOMACY_STATE.DECLARATION]: '선포',
|
||||
[DIPLOMACY_STATE.TRADE]: '교역',
|
||||
[DIPLOMACY_STATE.NON_AGGRESSION]: '불가침',
|
||||
};
|
||||
|
||||
const NATION_TARGET_COMMANDS = [
|
||||
'che_물자원조',
|
||||
'che_불가침제의',
|
||||
'che_선전포고',
|
||||
'che_종전제의',
|
||||
'che_불가침파기제의',
|
||||
] as const;
|
||||
|
||||
const nationAvailability = (
|
||||
action: (typeof NATION_TARGET_COMMANDS)[number],
|
||||
actorNationId: number,
|
||||
target: NationTargetSource
|
||||
): { available: boolean; reason: string } => {
|
||||
if (target.id === actorNationId) return { available: false, reason: '아국은 대상이 아닙니다.' };
|
||||
if (action === 'che_물자원조') {
|
||||
return target.diplomacyRestricted
|
||||
? { available: false, reason: '상대국이 외교제한 중입니다.' }
|
||||
: { available: true, reason: '현재 원조 대상' };
|
||||
}
|
||||
if (action === 'che_불가침제의') {
|
||||
const available = ![DIPLOMACY_STATE.WAR, DIPLOMACY_STATE.DECLARATION].includes(target.diplomacyState as 0 | 1);
|
||||
return { available, reason: available ? '현재 제의 가능' : '교전·선포 중에는 제의 불가' };
|
||||
}
|
||||
if (action === 'che_선전포고') {
|
||||
if (!target.adjacent) return { available: false, reason: '인접 국가가 아닙니다.' };
|
||||
const available = ![DIPLOMACY_STATE.WAR, DIPLOMACY_STATE.DECLARATION, DIPLOMACY_STATE.NON_AGGRESSION].includes(
|
||||
target.diplomacyState as 0 | 1 | 7
|
||||
);
|
||||
return { available, reason: available ? '현재 선전포고 가능' : '현재 외교 관계에서는 선전포고 불가' };
|
||||
}
|
||||
if (action === 'che_종전제의') {
|
||||
const available = [DIPLOMACY_STATE.WAR, DIPLOMACY_STATE.DECLARATION].includes(target.diplomacyState as 0 | 1);
|
||||
return { available, reason: available ? '현재 종전 제의 가능' : '전쟁·선포 중인 국가가 아닙니다.' };
|
||||
}
|
||||
const available = target.diplomacyState === DIPLOMACY_STATE.NON_AGGRESSION;
|
||||
return { available, reason: available ? '현재 불가침 파기 제의 가능' : '불가침 중인 국가가 아닙니다.' };
|
||||
};
|
||||
|
||||
/** 사령턴 외교 대상은 현재 명령에 맞는 국가부터 보이되, 예약 자체는 모든 대상을 유지한다. */
|
||||
export const buildRefNationTargetOptions = (options: {
|
||||
actorNationId: number;
|
||||
nations: readonly NationTargetSource[];
|
||||
}): RefNationTargetOptions => {
|
||||
const baseOptions = options.nations.map<TurnCommandOption>((entry) => ({
|
||||
value: entry.id,
|
||||
label: entry.name,
|
||||
color: entry.color,
|
||||
description: `수도 ${entry.capitalName} · 국력 ${entry.power.toLocaleString()} · 도시 ${entry.cityCount.toLocaleString()} · 장수 ${entry.generalCount.toLocaleString()}`,
|
||||
}));
|
||||
const nationTargets: Record<string, TurnCommandOption[]> = {};
|
||||
for (const action of NATION_TARGET_COMMANDS) {
|
||||
nationTargets[action] = options.nations
|
||||
.map((entry) => {
|
||||
const availability = nationAvailability(action, options.actorNationId, entry);
|
||||
const relation = DIPLOMACY_LABELS[entry.diplomacyState] ?? `관계 ${entry.diplomacyState}`;
|
||||
const term = entry.diplomacyTerm > 0 ? ` ${entry.diplomacyTerm}턴` : '';
|
||||
return {
|
||||
value: entry.id,
|
||||
label: entry.name,
|
||||
color: entry.color,
|
||||
availableNow: availability.available,
|
||||
description: `${availability.reason} · ${relation}${term} · 수도 ${entry.capitalName} · 국력 ${entry.power.toLocaleString()} · 도시 ${entry.cityCount.toLocaleString()} · 장수 ${entry.generalCount.toLocaleString()}`,
|
||||
power: entry.power,
|
||||
} as TurnCommandOption & { power: number };
|
||||
})
|
||||
.sort(
|
||||
(left, right) =>
|
||||
Number(right.availableNow) - Number(left.availableNow) ||
|
||||
right.power - left.power ||
|
||||
Number(left.value) - Number(right.value)
|
||||
)
|
||||
.map(({ power: _power, ...entry }) => entry);
|
||||
}
|
||||
return { nations: baseOptions, nationTargets };
|
||||
};
|
||||
|
||||
const RESOURCE_ACTION_GUIDE = [
|
||||
100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1200, 1500, 2000, 2500, 3000, 4000, 5000, 6000, 7000, 8000, 9000,
|
||||
10000,
|
||||
];
|
||||
|
||||
/** Ref SelectAmount의 dropdown 값을 공통 예약 입력 DTO로 옮긴다. */
|
||||
export const buildRefAmountPresets = (
|
||||
nationLevel: number,
|
||||
maxResourceActionAmount: number
|
||||
): Record<string, TurnCommandAmountPreset> => {
|
||||
const resourceMax = maxResourceActionAmount > 0 ? maxResourceActionAmount : 10_000;
|
||||
const resourceValues = RESOURCE_ACTION_GUIDE.filter((value) => value <= resourceMax);
|
||||
if (!resourceValues.includes(resourceMax)) resourceValues.push(resourceMax);
|
||||
const resourcePreset: TurnCommandAmountPreset = {
|
||||
values: resourceValues,
|
||||
defaultValue: Math.min(1000, resourceMax),
|
||||
min: Math.min(100, resourceMax),
|
||||
max: resourceMax,
|
||||
step: 1,
|
||||
};
|
||||
const aidMax = Math.max(10_000, Math.max(1, nationLevel) * 10_000);
|
||||
const aidPreset: TurnCommandAmountPreset = {
|
||||
values: Array.from({ length: Math.max(1, nationLevel) }, (_, index) => (index + 1) * 10_000),
|
||||
defaultValue: Math.min(1000, aidMax),
|
||||
min: 1000,
|
||||
max: aidMax,
|
||||
step: 10,
|
||||
};
|
||||
return {
|
||||
che_증여: resourcePreset,
|
||||
che_헌납: resourcePreset,
|
||||
che_군량매매: resourcePreset,
|
||||
che_포상: resourcePreset,
|
||||
che_몰수: resourcePreset,
|
||||
che_물자원조: aidPreset,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user