import type { City, General, Nation } from '@sammo-ts/logic/domain/entities.js'; import type { MapDefinition } from '@sammo-ts/logic/world/types.js'; import { allow, parsePercent, readCity, readDestCity, readDiplomacyState, readGeneral, readMetaNumberFromUnknown, resolveDestCityId, unknownOrDeny, } from './helpers.js'; import type { Constraint, RequirementKey } from './types.js'; export const occupiedCity = (options: { allowNeutral?: boolean } = {}): Constraint => ({ name: 'occupiedCity', requires: (ctx) => { const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }]; if (ctx.cityId !== undefined) { reqs.push({ kind: 'city', id: ctx.cityId }); } return reqs; }, test: (ctx, view) => { const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId }; if (!view.has(generalReq)) { return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.'); } const general = view.get(generalReq) as General | null; if (!general) { return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.'); } if (options.allowNeutral && general.nationId === 0) { return allow(); } const cityId = ctx.cityId ?? general.cityId; const cityReq: RequirementKey = { kind: 'city', id: cityId }; if (!view.has(cityReq)) { return unknownOrDeny(ctx, [cityReq], '도시 정보가 없습니다.'); } const city = view.get(cityReq) as City | null; if (!city) { return unknownOrDeny(ctx, [cityReq], '도시 정보가 없습니다.'); } if (city.nationId === general.nationId) { return allow(); } return { kind: 'deny', reason: '아국이 아닙니다.' }; }, }); export const occupiedDestCity = (): Constraint => ({ name: 'occupiedDestCity', requires: (ctx) => { const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }]; const destCityId = resolveDestCityId(ctx); if (destCityId !== undefined) { reqs.push({ kind: 'destCity', id: destCityId }); } return reqs; }, test: (ctx, view) => { const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId }; if (!view.has(generalReq)) { return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.'); } const general = view.get(generalReq) as General | null; if (!general) { return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.'); } const destCity = readDestCity(ctx, view); if (!destCity) { const destCityId = resolveDestCityId(ctx); if (destCityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'destCity', id: destCityId, }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } const baseNationId = ctx.nationId ?? general.nationId; if (destCity.nationId === baseNationId) { return allow(); } return { kind: 'deny', reason: '아국이 아닙니다.' }; }, }); export const suppliedCity = (): Constraint => ({ name: 'suppliedCity', requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []), test: (ctx, view) => { const city = readCity(view, ctx.cityId); if (!city) { if (ctx.cityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'city', id: ctx.cityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } if (city.supplyState) { return allow(); } return { kind: 'deny', reason: '고립된 도시입니다.' }; }, }); export const suppliedDestCity = (): Constraint => ({ name: 'suppliedDestCity', requires: (ctx) => resolveDestCityId(ctx) !== undefined ? [ { kind: 'destCity', id: resolveDestCityId(ctx) ?? 0, }, ] : [], test: (ctx, view) => { const destCity = readDestCity(ctx, view); if (!destCity) { const destCityId = resolveDestCityId(ctx); if (destCityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'destCity', id: destCityId, }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } if (destCity.supplyState) { return allow(); } return { kind: 'deny', reason: '고립된 도시입니다.' }; }, }); export const remainCityCapacity = (key: keyof City, label: string): Constraint => ({ name: 'remainCityCapacity', requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []), test: (ctx, view) => { const city = readCity(view, ctx.cityId); if (!city) { if (ctx.cityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'city', id: ctx.cityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } const maxKey = `${String(key)}Max` as keyof City; const current = city[key]; const max = city[maxKey]; if (typeof current !== 'number' || typeof max !== 'number') { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } if (current < max) { return allow(); } return { kind: 'deny', reason: `${label}이 충분합니다.` }; }, }); export const remainCityCapacityByMax = (key: keyof City, maxKey: keyof City, label: string): Constraint => ({ name: 'remainCityCapacityByMax', requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []), test: (ctx, view) => { const city = readCity(view, ctx.cityId); if (!city) { if (ctx.cityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'city', id: ctx.cityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } const current = city[key]; const max = city[maxKey]; if (typeof current !== 'number' || typeof max !== 'number') { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } if (current < max) { return allow(); } return { kind: 'deny', reason: `${label}이 충분합니다.` }; }, }); export const reqCityCapacity = (key: keyof City, label: string, required: number | string): Constraint => ({ name: 'reqCityCapacity', requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []), test: (ctx, view) => { const city = readCity(view, ctx.cityId); if (!city) { if (ctx.cityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'city', id: ctx.cityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } const current = city[key]; if (typeof current !== 'number') { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } if (typeof required === 'string') { const ratio = parsePercent(required); const maxKey = `${String(key)}Max` as keyof City; const max = city[maxKey]; if (ratio === null || typeof max !== 'number') { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } if (current >= max * ratio) { return allow(); } } else if (current >= required) { return allow(); } return { kind: 'deny', reason: `${label}이 부족합니다.` }; }, }); export const reqCityTrust = (minTrust: number): Constraint => ({ name: 'reqCityTrust', requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []), test: (ctx, view) => { const city = readCity(view, ctx.cityId); if (!city) { if (ctx.cityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'city', id: ctx.cityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } const trust = readMetaNumberFromUnknown(city.meta, 'trust') ?? null; if (trust === null) { return unknownOrDeny(ctx, [], '민심 정보가 없습니다.'); } if (trust >= minTrust) { return allow(); } return { kind: 'deny', reason: '민심이 낮아 주민들이 도망갑니다.' }; }, }); export const existsDestCity = (): Constraint => ({ name: 'existsDestCity', requires: (ctx) => resolveDestCityId(ctx) !== undefined ? [ { kind: 'destCity', id: resolveDestCityId(ctx) ?? 0, }, ] : [], test: (ctx, view) => { const destCityId = resolveDestCityId(ctx); if (destCityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'destCity', id: destCityId }; if (!view.has(req)) { return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } const city = view.get(req) as City | null; if (!city) { return { kind: 'deny', reason: '도시 정보가 없습니다.' }; } return allow(); }, }); export const notOccupiedDestCity = (): Constraint => ({ name: 'notOccupiedDestCity', requires: (ctx) => { const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }]; const destCityId = resolveDestCityId(ctx); if (destCityId !== undefined) { reqs.push({ kind: 'destCity', id: destCityId }); } return reqs; }, test: (ctx, view) => { const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId }; if (!view.has(generalReq)) { return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.'); } const general = view.get(generalReq) as General | null; if (!general) { return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.'); } const destCity = readDestCity(ctx, view); if (!destCity) { const destCityId = resolveDestCityId(ctx); if (destCityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'destCity', id: destCityId, }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } if (destCity.nationId !== general.nationId) { return allow(); } return { kind: 'deny', reason: '아국입니다.' }; }, }); export const notNeutralDestCity = (): Constraint => ({ name: 'notNeutralDestCity', requires: (ctx) => resolveDestCityId(ctx) !== undefined ? [ { kind: 'destCity', id: resolveDestCityId(ctx) ?? 0, }, ] : [], test: (ctx, view) => { const destCity = readDestCity(ctx, view); if (!destCity) { const destCityId = resolveDestCityId(ctx); if (destCityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'destCity', id: destCityId, }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } if (destCity.nationId !== 0) { return allow(); } return { kind: 'deny', reason: '공백지입니다.' }; }, }); export const notSameDestCity = (): Constraint => ({ name: 'notSameDestCity', requires: (ctx) => { const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }]; const destCityId = resolveDestCityId(ctx); if (destCityId !== undefined) { reqs.push({ kind: 'destCity', id: destCityId }); } return reqs; }, test: (ctx, view) => { const general = readGeneral(ctx, view); if (!general) { const req: RequirementKey = { kind: 'general', id: ctx.actorId }; return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.'); } const destCityId = resolveDestCityId(ctx); if (destCityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } if (general.cityId !== destCityId) { return allow(); } return { kind: 'deny', reason: '같은 도시입니다.' }; }, }); export const hasRouteWithEnemy = (): Constraint => ({ name: 'hasRouteWithEnemy', requires: (ctx) => { const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }]; const destCityId = resolveDestCityId(ctx); if (destCityId !== undefined) { reqs.push({ kind: 'destCity', id: destCityId }); } reqs.push({ kind: 'env', key: 'map' }); reqs.push({ kind: 'env', key: 'cities' }); reqs.push({ kind: 'env', key: 'nations' }); return reqs; }, test: (ctx, view) => { const general = readGeneral(ctx, view); if (!general) { const req: RequirementKey = { kind: 'general', id: ctx.actorId }; return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.'); } const destCity = readDestCity(ctx, view); if (!destCity) { const destCityId = resolveDestCityId(ctx); if (destCityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'destCity', id: destCityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } const map = view.get({ kind: 'env', key: 'map' }) as MapDefinition | null; const cities = view.get({ kind: 'env', key: 'cities' }) as City[] | null; const nations = view.get({ kind: 'env', key: 'nations' }) as Array<{ id: number }> | null; if (!map || !cities || !nations) { return unknownOrDeny(ctx, [], '경로 정보가 없습니다.'); } const allowedNationIds = new Set(); allowedNationIds.add(general.nationId); allowedNationIds.add(0); for (const nation of nations) { const state = readDiplomacyState(view, general.nationId, nation.id); if (state === 0) { allowedNationIds.add(nation.id); } } if ( destCity.nationId !== 0 && destCity.nationId !== general.nationId && !allowedNationIds.has(destCity.nationId) ) { return { kind: 'deny', reason: '교전중인 국가가 아닙니다.' }; } const allowedCityIds = new Set(); for (const city of cities) { if (allowedNationIds.has(city.nationId)) { allowedCityIds.add(city.id); } } if (!allowedCityIds.has(destCity.id)) { return { kind: 'deny', reason: '경로에 도달할 방법이 없습니다.' }; } return allow(); }, }); export const beNeutralCity = (): Constraint => ({ name: 'beNeutralCity', requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []), test: (ctx, view) => { const city = readCity(view, ctx.cityId); if (!city) { if (ctx.cityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'city', id: ctx.cityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } if (city.nationId === 0) { return allow(); } const general = readGeneral(ctx, view); if (general && city.nationId === general.nationId) { const nationReq: RequirementKey = { kind: 'nation', id: general.nationId }; const nation = view.get(nationReq) as Nation | null; if (nation && nation.level === 0) { // 방랑군 본인 도시면 건국 가능 return allow(); } } return { kind: 'deny', reason: '공백지가 아닙니다.' }; }, }); export const reqCityLevel = (levels: number[]): Constraint => ({ name: 'reqCityLevel', requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []), test: (ctx, view) => { const city = readCity(view, ctx.cityId); if (!city) { if (ctx.cityId === undefined) { return unknownOrDeny(ctx, [], '도시 정보가 없습니다.'); } const req: RequirementKey = { kind: 'city', id: ctx.cityId }; return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.'); } if (levels.includes(city.level)) { return allow(); } return { kind: 'deny', reason: '규모가 맞지 않습니다.' }; }, });