485 lines
20 KiB
JavaScript
485 lines
20 KiB
JavaScript
import { createServer } from 'vite';
|
|
|
|
const server = await createServer({
|
|
logLevel: 'error',
|
|
server: { middlewareMode: true },
|
|
appType: 'custom'
|
|
});
|
|
|
|
try {
|
|
const { battleScenarios } = await server.ssrLoadModule('/src/game/data/battles.ts');
|
|
const { campaignRecruitUnits } = await server.ssrLoadModule('/src/game/data/scenario.ts');
|
|
const { unitClasses, terrainRules } = await server.ssrLoadModule('/src/game/data/battleRules.ts');
|
|
const { itemCatalog, equipmentSlots } = await server.ssrLoadModule('/src/game/data/battleItems.ts');
|
|
const { sortieFormationRoles } = await server.ssrLoadModule('/src/game/data/sortieDeployment.ts');
|
|
|
|
const scenarioEntries = Object.entries(battleScenarios);
|
|
const scenarioIds = new Set(scenarioEntries.map(([id]) => id));
|
|
const classKeys = new Set(Object.keys(unitClasses));
|
|
const terrainKeys = new Set(Object.keys(terrainRules));
|
|
const itemKeys = new Set(Object.keys(itemCatalog));
|
|
const itemNames = new Set(Object.values(itemCatalog).map((item) => item.name));
|
|
const formationRoles = new Set(sortieFormationRoles);
|
|
const knownRewardUnitIds = new Set([
|
|
...scenarioEntries.flatMap(([, scenario]) => scenario.units.map((unit) => unit.id)),
|
|
...(campaignRecruitUnits ?? []).map((unit) => unit.id)
|
|
]);
|
|
const errors = [];
|
|
|
|
scenarioEntries.forEach(([scenarioId, scenario]) => {
|
|
const context = scenarioId;
|
|
if (scenario.id !== scenarioId) {
|
|
errors.push(`${context}: scenario id does not match battleScenarios key`);
|
|
}
|
|
assertNonEmptyString(errors, scenario.title, `${context}: title`);
|
|
assertNonEmptyString(errors, scenario.victoryConditionLabel, `${context}: victory condition label`);
|
|
assertNonEmptyString(errors, scenario.defeatConditionLabel, `${context}: defeat condition label`);
|
|
|
|
validateMap(errors, scenario, terrainKeys, context);
|
|
validateUnits(errors, scenario, classKeys, itemKeys, equipmentSlots, context);
|
|
validateObjectives(errors, scenario, context);
|
|
validateSortie(errors, scenario, classKeys, formationRoles, terrainRules, context);
|
|
validateRewards(errors, scenario, scenarioIds, itemNames, knownRewardUnitIds, context);
|
|
});
|
|
|
|
if (errors.length) {
|
|
console.error(`Battle scenario data verification failed with ${errors.length} issue(s):`);
|
|
errors.slice(0, 80).forEach((error) => console.error(`- ${error}`));
|
|
if (errors.length > 80) {
|
|
console.error(`- ...and ${errors.length - 80} more`);
|
|
}
|
|
process.exitCode = 1;
|
|
} else {
|
|
const unitCount = scenarioEntries.reduce((total, [, scenario]) => total + scenario.units.length, 0);
|
|
const rewardCount = scenarioEntries.reduce(
|
|
(total, [, scenario]) => total + scenario.itemRewards.length + campaignRewardLabelCount(scenario.campaignReward),
|
|
0
|
|
);
|
|
console.log(
|
|
`Verified ${scenarioEntries.length} battle scenarios, ${unitCount} unit placements, maps, objectives, sortie data, equipment, and ${rewardCount} reward labels.`
|
|
);
|
|
}
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
|
|
function validateMap(errors, scenario, terrainKeys, context) {
|
|
const { map } = scenario;
|
|
if (!map || !Number.isInteger(map.width) || !Number.isInteger(map.height) || map.width < 1 || map.height < 1) {
|
|
errors.push(`${context}: invalid map dimensions`);
|
|
return;
|
|
}
|
|
if (!Array.isArray(map.terrain) || map.terrain.length !== map.height) {
|
|
errors.push(`${context}: terrain row count does not match map height`);
|
|
return;
|
|
}
|
|
|
|
map.terrain.forEach((row, y) => {
|
|
if (!Array.isArray(row) || row.length !== map.width) {
|
|
errors.push(`${context}: terrain row ${y} does not match map width`);
|
|
return;
|
|
}
|
|
row.forEach((terrain, x) => {
|
|
if (!terrainKeys.has(terrain)) {
|
|
errors.push(`${context}: unknown terrain "${terrain}" at ${x},${y}`);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function validateUnits(errors, scenario, classKeys, itemKeys, equipmentSlots, context) {
|
|
const unitIds = new Set();
|
|
const occupiedTiles = new Map();
|
|
const allies = [];
|
|
const enemies = [];
|
|
|
|
scenario.units.forEach((unit) => {
|
|
const unitContext = `${context}/${unit.id}`;
|
|
if (!unit.id) {
|
|
errors.push(`${context}: unit without id`);
|
|
return;
|
|
}
|
|
if (unitIds.has(unit.id)) {
|
|
errors.push(`${context}: duplicate unit id "${unit.id}"`);
|
|
}
|
|
unitIds.add(unit.id);
|
|
|
|
if (unit.faction === 'ally') {
|
|
allies.push(unit);
|
|
} else if (unit.faction === 'enemy') {
|
|
enemies.push(unit);
|
|
} else {
|
|
errors.push(`${unitContext}: invalid faction "${unit.faction}"`);
|
|
}
|
|
|
|
if (!classKeys.has(unit.classKey)) {
|
|
errors.push(`${unitContext}: unknown classKey "${unit.classKey}"`);
|
|
}
|
|
validateCharacterProgress(errors, unit, unitContext);
|
|
if (!Number.isInteger(unit.hp) || !Number.isInteger(unit.maxHp) || unit.hp < 1 || unit.maxHp < 1 || unit.hp > unit.maxHp) {
|
|
errors.push(`${unitContext}: invalid hp ${unit.hp}/${unit.maxHp}`);
|
|
}
|
|
if (!Number.isInteger(unit.move) || unit.move < 1) {
|
|
errors.push(`${unitContext}: invalid move ${unit.move}`);
|
|
}
|
|
if (!isInBounds(unit, scenario.map)) {
|
|
errors.push(`${unitContext}: position ${unit.x},${unit.y} is outside ${scenario.map.width}x${scenario.map.height}`);
|
|
} else {
|
|
const tileKey = `${unit.x},${unit.y}`;
|
|
const existingUnitId = occupiedTiles.get(tileKey);
|
|
if (existingUnitId) {
|
|
errors.push(`${context}: units "${existingUnitId}" and "${unit.id}" share tile ${tileKey}`);
|
|
}
|
|
occupiedTiles.set(tileKey, unit.id);
|
|
}
|
|
|
|
equipmentSlots.forEach((slot) => {
|
|
const equipment = unit.equipment?.[slot];
|
|
if (!equipment) {
|
|
errors.push(`${unitContext}: missing ${slot} equipment`);
|
|
return;
|
|
}
|
|
if (!itemKeys.has(equipment.itemId)) {
|
|
errors.push(`${unitContext}: unknown ${slot} item "${equipment.itemId}"`);
|
|
}
|
|
if (!Number.isInteger(equipment.level) || equipment.level < 1 || equipment.level > 9) {
|
|
errors.push(`${unitContext}: invalid ${slot} level ${equipment.level}`);
|
|
}
|
|
if (!Number.isInteger(equipment.exp) || equipment.exp < 0) {
|
|
errors.push(`${unitContext}: invalid ${slot} exp ${equipment.exp}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
if (!allies.length) {
|
|
errors.push(`${context}: scenario has no allied units`);
|
|
}
|
|
if (!enemies.length) {
|
|
errors.push(`${context}: scenario has no enemy units`);
|
|
}
|
|
}
|
|
|
|
function validateObjectives(errors, scenario, context) {
|
|
const unitIds = new Set(scenario.units.map((unit) => unit.id));
|
|
const objectiveIds = new Set();
|
|
|
|
if (!scenario.leaderUnitId || !unitIds.has(scenario.leaderUnitId)) {
|
|
errors.push(`${context}: leaderUnitId "${scenario.leaderUnitId}" is missing from units`);
|
|
}
|
|
scenario.defeatConditions.forEach((condition) => {
|
|
if (!unitIds.has(condition.unitId)) {
|
|
errors.push(`${context}: defeat condition references missing unit "${condition.unitId}"`);
|
|
}
|
|
});
|
|
|
|
scenario.objectives.forEach((objective) => {
|
|
if (!objective.id || objectiveIds.has(objective.id)) {
|
|
errors.push(`${context}: duplicate or missing objective id "${objective.id}"`);
|
|
}
|
|
objectiveIds.add(objective.id);
|
|
assertNonEmptyString(errors, objective.label, `${context}/${objective.id}: objective label`);
|
|
if (!Number.isFinite(objective.rewardGold) || objective.rewardGold < 0) {
|
|
errors.push(`${context}/${objective.id}: invalid rewardGold ${objective.rewardGold}`);
|
|
}
|
|
if (objective.unitId && !unitIds.has(objective.unitId)) {
|
|
errors.push(`${context}/${objective.id}: references missing unit "${objective.unitId}"`);
|
|
}
|
|
if (objective.targetTile && !isInBounds(objective.targetTile, scenario.map)) {
|
|
errors.push(`${context}/${objective.id}: target tile ${objective.targetTile.x},${objective.targetTile.y} is outside map`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function validateCharacterProgress(errors, unit, context) {
|
|
if (!Number.isInteger(unit.level) || unit.level < 1) {
|
|
errors.push(`${context}: invalid level ${unit.level}`);
|
|
}
|
|
if (!Number.isInteger(unit.exp) || unit.exp < 0) {
|
|
errors.push(`${context}: invalid exp ${unit.exp}`);
|
|
} else if (Number.isInteger(unit.level) && unit.level < 99 && unit.exp >= 100) {
|
|
errors.push(`${context}: exp ${unit.exp} should be below 100 before max level`);
|
|
} else if (Number.isInteger(unit.level) && unit.level >= 99 && unit.exp > 100) {
|
|
errors.push(`${context}: max level exp ${unit.exp} should be at most 100`);
|
|
}
|
|
}
|
|
|
|
function validateSortie(errors, scenario, classKeys, formationRoles, terrainRules, context) {
|
|
const sortie = scenario.sortie;
|
|
if (!sortie) {
|
|
return;
|
|
}
|
|
|
|
const unitIds = new Set(scenario.units.map((unit) => unit.id));
|
|
const allyUnitIds = new Set(scenario.units.filter((unit) => unit.faction === 'ally').map((unit) => unit.id));
|
|
const enemyTiles = new Set(scenario.units.filter((unit) => unit.faction === 'enemy').map((unit) => `${unit.x},${unit.y}`));
|
|
if (!Number.isInteger(sortie.sortieLimit) || sortie.sortieLimit < 1) {
|
|
errors.push(`${context}: invalid sortie limit ${sortie.sortieLimit}`);
|
|
}
|
|
if ((sortie.requiredUnits?.length ?? 0) > sortie.sortieLimit) {
|
|
errors.push(`${context}: requiredUnits count exceeds sortieLimit`);
|
|
}
|
|
if ((sortie.recommendedUnits?.length ?? 0) > sortie.sortieLimit) {
|
|
errors.push(`${context}: recommendedUnits count exceeds sortieLimit`);
|
|
}
|
|
|
|
const requiredUnits = sortie.requiredUnits ?? [];
|
|
const excludedUnits = sortie.excludedUnits ?? [];
|
|
const requiredUnitIds = new Set();
|
|
const excludedUnitIds = new Set();
|
|
|
|
requiredUnits.forEach((unitId) => {
|
|
if (requiredUnitIds.has(unitId)) {
|
|
errors.push(`${context}: duplicate required unit "${unitId}"`);
|
|
}
|
|
requiredUnitIds.add(unitId);
|
|
});
|
|
excludedUnits.forEach((unitId) => {
|
|
if (excludedUnitIds.has(unitId)) {
|
|
errors.push(`${context}: duplicate excluded unit "${unitId}"`);
|
|
}
|
|
excludedUnitIds.add(unitId);
|
|
if (requiredUnitIds.has(unitId)) {
|
|
errors.push(`${context}: unit "${unitId}" cannot be both required and excluded`);
|
|
}
|
|
});
|
|
|
|
[...requiredUnits, ...excludedUnits].forEach((unitId) => {
|
|
if (!unitIds.has(unitId)) {
|
|
errors.push(`${context}: sortie references missing unit "${unitId}"`);
|
|
} else if (!allyUnitIds.has(unitId)) {
|
|
errors.push(`${context}: sortie references non-allied unit "${unitId}"`);
|
|
}
|
|
});
|
|
const recommendedUnitIds = new Set();
|
|
const recommendedRoleByUnitId = new Map();
|
|
sortie.recommendedUnits.forEach((recommendation) => {
|
|
if (recommendedUnitIds.has(recommendation.unitId)) {
|
|
errors.push(`${context}: duplicate recommended unit "${recommendation.unitId}"`);
|
|
}
|
|
recommendedUnitIds.add(recommendation.unitId);
|
|
if (!unitIds.has(recommendation.unitId)) {
|
|
errors.push(`${context}: recommended unit "${recommendation.unitId}" is missing from units`);
|
|
} else if (!allyUnitIds.has(recommendation.unitId)) {
|
|
errors.push(`${context}: recommended unit "${recommendation.unitId}" is not allied`);
|
|
}
|
|
if (excludedUnitIds.has(recommendation.unitId)) {
|
|
errors.push(`${context}: excluded unit "${recommendation.unitId}" cannot also be recommended`);
|
|
}
|
|
if (recommendation.role && !formationRoles.has(recommendation.role)) {
|
|
errors.push(`${context}/${recommendation.unitId}: invalid recommendation role "${recommendation.role}"`);
|
|
}
|
|
if (recommendation.role) {
|
|
recommendedRoleByUnitId.set(recommendation.unitId, recommendation.role);
|
|
}
|
|
assertNonEmptyString(errors, recommendation.reason, `${context}/${recommendation.unitId}: recommendation reason`);
|
|
});
|
|
sortie.recommendedClasses.forEach((recommendation) => {
|
|
assertNonEmptyString(errors, recommendation.label, `${context}: recommended class label`);
|
|
assertNonEmptyString(errors, recommendation.reason, `${context}/${recommendation.label}: recommended class reason`);
|
|
const recommendedClassKeys = Array.isArray(recommendation.classKeys) ? recommendation.classKeys : [];
|
|
if (!Array.isArray(recommendation.classKeys) || recommendation.classKeys.length === 0) {
|
|
errors.push(`${context}/${recommendation.label}: recommended class must include at least one classKey`);
|
|
}
|
|
const availableRecommendedUnits = scenario.units.filter(
|
|
(unit) => unit.faction === 'ally' && !excludedUnitIds.has(unit.id) && recommendedClassKeys.includes(unit.classKey)
|
|
);
|
|
if (availableRecommendedUnits.length === 0) {
|
|
errors.push(`${context}/${recommendation.label}: recommended class has no available allied unit`);
|
|
}
|
|
recommendedClassKeys.forEach((classKey) => {
|
|
if (!classKeys.has(classKey)) {
|
|
errors.push(`${context}: recommended class "${recommendation.label}" references unknown classKey "${classKey}"`);
|
|
}
|
|
});
|
|
});
|
|
|
|
if (sortie.deploymentSlots.length !== sortie.sortieLimit) {
|
|
errors.push(
|
|
`${context}: deploymentSlots count ${sortie.deploymentSlots.length} does not match sortieLimit ${sortie.sortieLimit}`
|
|
);
|
|
}
|
|
const deploymentTiles = new Set();
|
|
const deploymentUnitIds = new Set();
|
|
sortie.deploymentSlots.forEach((slot) => {
|
|
if (!isInBounds(slot, scenario.map)) {
|
|
errors.push(`${context}: deployment slot ${slot.x},${slot.y} is outside map`);
|
|
return;
|
|
}
|
|
if (slot.role && !formationRoles.has(slot.role)) {
|
|
errors.push(`${context}: deployment slot ${slot.x},${slot.y} has invalid role "${slot.role}"`);
|
|
}
|
|
const tileKey = `${slot.x},${slot.y}`;
|
|
const terrain = scenario.map.terrain[slot.y][slot.x];
|
|
if (terrainRules[terrain]?.passable === false) {
|
|
errors.push(`${context}: deployment slot ${tileKey} is on impassable terrain "${terrain}"`);
|
|
}
|
|
if (enemyTiles.has(tileKey)) {
|
|
errors.push(`${context}: deployment slot ${tileKey} overlaps an enemy unit`);
|
|
}
|
|
if (deploymentTiles.has(tileKey)) {
|
|
errors.push(`${context}: duplicate deployment slot ${tileKey}`);
|
|
}
|
|
deploymentTiles.add(tileKey);
|
|
if (slot.unitId && !unitIds.has(slot.unitId)) {
|
|
errors.push(`${context}: deployment slot references missing unit "${slot.unitId}"`);
|
|
} else if (slot.unitId && !allyUnitIds.has(slot.unitId)) {
|
|
errors.push(`${context}: deployment slot references non-allied unit "${slot.unitId}"`);
|
|
}
|
|
if (slot.unitId && excludedUnitIds.has(slot.unitId)) {
|
|
errors.push(`${context}: excluded unit "${slot.unitId}" cannot have a deployment slot`);
|
|
}
|
|
if (slot.unitId && !requiredUnitIds.has(slot.unitId) && !recommendedUnitIds.has(slot.unitId)) {
|
|
errors.push(`${context}: deployment slot unit "${slot.unitId}" must be required or recommended`);
|
|
}
|
|
const recommendedRole = slot.unitId ? recommendedRoleByUnitId.get(slot.unitId) : undefined;
|
|
if (slot.role && recommendedRole && slot.role !== recommendedRole) {
|
|
errors.push(
|
|
`${context}: deployment slot unit "${slot.unitId}" role "${slot.role}" does not match recommendation role "${recommendedRole}"`
|
|
);
|
|
}
|
|
if (slot.unitId) {
|
|
if (deploymentUnitIds.has(slot.unitId)) {
|
|
errors.push(`${context}: duplicate deployment unit slot "${slot.unitId}"`);
|
|
}
|
|
deploymentUnitIds.add(slot.unitId);
|
|
}
|
|
});
|
|
requiredUnits.forEach((unitId) => {
|
|
if (!deploymentUnitIds.has(unitId)) {
|
|
errors.push(`${context}: required unit "${unitId}" must have an explicit deployment slot`);
|
|
}
|
|
});
|
|
recommendedUnitIds.forEach((unitId) => {
|
|
if (!deploymentUnitIds.has(unitId)) {
|
|
errors.push(`${context}: recommended unit "${unitId}" must have an explicit deployment slot`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function validateRewards(errors, scenario, scenarioIds, itemNames, knownRewardUnitIds, context) {
|
|
validateRewardLabels(errors, scenario.itemRewards, `${context}: itemRewards`);
|
|
|
|
const campaignReward = scenario.campaignReward;
|
|
if (campaignReward) {
|
|
validateRewardLabels(errors, campaignReward.supplies ?? [], `${context}: campaignReward.supplies`);
|
|
validateRewardLabels(errors, campaignReward.equipment ?? [], `${context}: campaignReward.equipment`);
|
|
validateRewardLabels(errors, campaignReward.reputation ?? [], `${context}: campaignReward.reputation`, { allowSignedAmount: true });
|
|
validateRecruitRewards(errors, campaignReward.recruits ?? [], knownRewardUnitIds, context);
|
|
validateEquipmentRewardLabels(errors, campaignReward.equipment ?? [], itemNames, context);
|
|
if (campaignReward.unlockLabel !== undefined) {
|
|
assertNonEmptyString(errors, campaignReward.unlockLabel, `${context}: campaignReward unlockLabel`);
|
|
}
|
|
if (campaignReward.note !== undefined) {
|
|
assertNonEmptyString(errors, campaignReward.note, `${context}: campaignReward note`);
|
|
}
|
|
}
|
|
|
|
const unlockBattleId = campaignReward?.unlockBattleId;
|
|
if (unlockBattleId && !scenarioIds.has(unlockBattleId)) {
|
|
errors.push(`${context}: campaignReward unlockBattleId "${unlockBattleId}" is not a known battle`);
|
|
}
|
|
}
|
|
|
|
function validateRewardLabels(errors, rewards, context, options = {}) {
|
|
if (!Array.isArray(rewards)) {
|
|
errors.push(`${context} must be an array`);
|
|
return;
|
|
}
|
|
|
|
const labels = new Set();
|
|
rewards.forEach((reward, index) => {
|
|
assertNonEmptyString(errors, reward, `${context}[${index}]`);
|
|
if (typeof reward !== 'string') {
|
|
return;
|
|
}
|
|
|
|
const parsed = parseRewardLabel(reward);
|
|
if (!parsed.label) {
|
|
errors.push(`${context}[${index}] has an empty parsed label`);
|
|
}
|
|
if (!Number.isInteger(parsed.amount) || parsed.amount < 1) {
|
|
if (!options.allowSignedAmount || !Number.isInteger(parsed.amount) || parsed.amount === 0) {
|
|
errors.push(`${context}[${index}] has invalid amount ${parsed.amount}`);
|
|
}
|
|
}
|
|
if (labels.has(parsed.label)) {
|
|
errors.push(`${context}: duplicate reward label "${parsed.label}"`);
|
|
}
|
|
labels.add(parsed.label);
|
|
});
|
|
}
|
|
|
|
function validateRecruitRewards(errors, recruits, knownRewardUnitIds, context) {
|
|
const recruitIds = new Set();
|
|
recruits.forEach((unitId, index) => {
|
|
assertNonEmptyString(errors, unitId, `${context}: campaignReward.recruits[${index}]`);
|
|
if (typeof unitId !== 'string') {
|
|
return;
|
|
}
|
|
if (!knownRewardUnitIds.has(unitId)) {
|
|
errors.push(`${context}: campaignReward recruit "${unitId}" is not a known battle or campaign recruit unit`);
|
|
}
|
|
if (recruitIds.has(unitId)) {
|
|
errors.push(`${context}: duplicate campaignReward recruit "${unitId}"`);
|
|
}
|
|
recruitIds.add(unitId);
|
|
});
|
|
}
|
|
|
|
function validateEquipmentRewardLabels(errors, equipmentRewards, itemNames, context) {
|
|
equipmentRewards.forEach((reward) => {
|
|
const { label } = parseRewardLabel(reward);
|
|
if (itemNames.has(label) || looksLikeCampaignRecord(label)) {
|
|
return;
|
|
}
|
|
errors.push(`${context}: campaignReward equipment "${reward}" is neither an equipment catalog item nor a campaign record/map label`);
|
|
});
|
|
}
|
|
|
|
function parseRewardLabel(reward) {
|
|
const normalized = String(reward).replace(/\s+/g, ' ').trim();
|
|
const match = normalized.match(/^(.*?)(?:\s*[+xX]?\s*(\d+))$/);
|
|
if (!match) {
|
|
return { label: normalized, amount: 1 };
|
|
}
|
|
|
|
return {
|
|
label: match[1].trim() || normalized,
|
|
amount: Number(match[2] ?? 1)
|
|
};
|
|
}
|
|
|
|
function looksLikeCampaignRecord(label) {
|
|
return /기록|문서|장부|지도|표식|서찰|명단|명부|초안|부적|군령패|통행문서|방화선|말뚝|항복문|물길|병법서|봉화도|외곽도|수위표|성문도|정찰|진군|퇴각|회유|서약|고시문|길잡이|소문|풍문|논의|준비|명성|공적|민심|신뢰|명분|예우|공훈/.test(label);
|
|
}
|
|
|
|
function campaignRewardLabelCount(campaignReward) {
|
|
if (!campaignReward) {
|
|
return 0;
|
|
}
|
|
return (
|
|
(campaignReward.supplies?.length ?? 0) +
|
|
(campaignReward.equipment?.length ?? 0) +
|
|
(campaignReward.reputation?.length ?? 0) +
|
|
(campaignReward.recruits?.length ?? 0) +
|
|
(campaignReward.unlockBattleId ? 1 : 0)
|
|
);
|
|
}
|
|
|
|
function isInBounds(point, map) {
|
|
return (
|
|
Number.isInteger(point.x) &&
|
|
Number.isInteger(point.y) &&
|
|
point.x >= 0 &&
|
|
point.y >= 0 &&
|
|
point.x < map.width &&
|
|
point.y < map.height
|
|
);
|
|
}
|
|
|
|
function assertNonEmptyString(errors, value, context) {
|
|
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
errors.push(`${context} must be a non-empty string`);
|
|
}
|
|
}
|