483 lines
18 KiB
TypeScript
483 lines
18 KiB
TypeScript
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<number>();
|
|
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<number>();
|
|
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: '규모가 맞지 않습니다.' };
|
|
},
|
|
});
|