feat: add encounter map
This commit is contained in:
parent
2a4383ff10
commit
88eeee6ab7
|
|
@ -0,0 +1,26 @@
|
||||||
|
# npc encounter (2): offer random trades, could be merchants or healer or something
|
||||||
|
# shelter (2): offer consumable restock and heal
|
||||||
|
# enemy (10): minor enemies
|
||||||
|
# elite (4): dangerous enemies
|
||||||
|
# boss (1): boss enemy
|
||||||
|
type,name,description
|
||||||
|
'npc'|'enemy'|'elite'|'boss'|'event'|'shelter',string,string
|
||||||
|
enemy,仙人掌怪,概念:防+强化。【尖刺X】:对攻击者造成X点伤害。
|
||||||
|
enemy,蛇,概念:攻+强化。给玩家塞入蛇毒牌(消耗。一回合弃掉超过1张蛇毒时,受到6伤害)。
|
||||||
|
enemy,木乃伊,概念:攻+防。【诅咒】:受攻击时物品【攻击】-1,直到弃掉一张该物品的牌。
|
||||||
|
enemy,枪手,概念:单回高攻。【瞄准X】:造成双倍伤害。受伤时失去等量【瞄准】。
|
||||||
|
enemy,风卷草,概念:防+强化。【滚动X】:攻击时,每消耗10点【滚动】,造成等量伤害。
|
||||||
|
enemy,秃鹫,概念:攻+防。造成伤害后玩家获得秃鹫之眼(当你受到伤害时自动从手牌打出受到秃鹫的攻击)。
|
||||||
|
enemy,沙蝎,概念:攻+强化。【尾刺X】:玩家回合结束时受到沙蝎的X点攻击。受伤时失去等量【尾刺】。
|
||||||
|
enemy,幼沙虫,概念:防+强化。每回合第一次受伤时,玩家失去1点能量。
|
||||||
|
enemy,蜥蜴,概念:攻+防+逃跑。【脱皮】:若脱皮达到生命上限,则怪物逃跑,玩家不能获得战斗奖励。
|
||||||
|
enemy,沙匪,概念:攻特化。洗牌时,将一个随机物品的牌全部弃掉。
|
||||||
|
elite,风暴之灵,【风暴X】:攻击时,玩家获得1张静电。受伤时失去等量【风暴】。(静电:在手里时受【电击】伤害+1)
|
||||||
|
elite,骑马枪手,【冲锋X】:受到或造成的伤害翻倍并消耗等量的冲锋。
|
||||||
|
elite,沙虫王,召唤幼体沙虫;每当玩家弃掉一张牌,恢复1生命。
|
||||||
|
elite,沙漠守卫,召唤木乃伊;会复活木乃伊2次。
|
||||||
|
boss,法老之灵,沙漠区域最终Boss。
|
||||||
|
npc,沙漠商人,商店:可以恢复生命、出售装备、附魔物品。
|
||||||
|
npc,绿洲篝火,篝火:可以恢复生命、补充药水使用次数、获得下次战斗Buff。
|
||||||
|
npc,迷失的旅人,提供任务:完成特定地点遭遇以获得独特奖励。
|
||||||
|
event,海市蜃楼,随机遭遇:可能获得宝藏或遭遇陷阱,使用d6双阶段结构结算。
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
type EncounterDesertTable = readonly {
|
||||||
|
readonly type: "npc" | "enemy" | "elite" | "boss" | "event" | "shelter";
|
||||||
|
readonly name: string;
|
||||||
|
readonly description: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export type EncounterDesert = EncounterDesertTable[number];
|
||||||
|
|
||||||
|
declare const data: EncounterDesertTable;
|
||||||
|
export default data;
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
import heroItemFighter1Csv from './heroItemFighter1.csv';
|
import heroItemFighter1Csv from './heroItemFighter1.csv';
|
||||||
|
import encounterDesertCsv from './encounterDesert.csv';
|
||||||
|
|
||||||
export const heroItemFighter1Data = heroItemFighter1Csv;
|
export const heroItemFighter1Data = heroItemFighter1Csv;
|
||||||
|
export const encounterDesertData = encounterDesertCsv;
|
||||||
|
export { default as encounterDesertCsv, type EncounterDesert } from './encounterDesert.csv';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,263 @@
|
||||||
|
import { Mulberry32RNG, type RNG } from '@/utils/rng';
|
||||||
|
import encounterDesertCsv, { type EncounterDesert } from '../data/encounterDesert.csv';
|
||||||
|
import { MapNodeType } from './types';
|
||||||
|
import type { MapLayer, MapNode, PointCrawlMap } from './types';
|
||||||
|
|
||||||
|
/** Cache for parsed encounters by type */
|
||||||
|
const encountersByType = new Map<string, EncounterDesert[]>();
|
||||||
|
|
||||||
|
function indexEncounters(): void {
|
||||||
|
if (encountersByType.size > 0) return;
|
||||||
|
|
||||||
|
for (const encounter of encounterDesertCsv) {
|
||||||
|
const type = encounter.type;
|
||||||
|
if (!encountersByType.has(type)) {
|
||||||
|
encountersByType.set(type, []);
|
||||||
|
}
|
||||||
|
encountersByType.get(type)!.push(encounter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map from MapNodeType to encounter type key */
|
||||||
|
const NODE_TYPE_TO_ENCOUNTER: Partial<Record<MapNodeType, string>> = {
|
||||||
|
[MapNodeType.Combat]: 'enemy',
|
||||||
|
[MapNodeType.Elite]: 'elite',
|
||||||
|
[MapNodeType.Boss]: 'boss',
|
||||||
|
[MapNodeType.Event]: 'event',
|
||||||
|
[MapNodeType.NPC]: 'npc',
|
||||||
|
[MapNodeType.Shelter]: 'shelter',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks a random encounter for the given node type.
|
||||||
|
* Returns undefined if no matching encounter exists.
|
||||||
|
*/
|
||||||
|
function pickEncounterForNode(type: MapNodeType, rng: RNG): EncounterDesert | undefined {
|
||||||
|
indexEncounters();
|
||||||
|
const encounterType = NODE_TYPE_TO_ENCOUNTER[type];
|
||||||
|
if (!encounterType) return undefined;
|
||||||
|
|
||||||
|
const pool = encountersByType.get(encounterType);
|
||||||
|
if (!pool || pool.length === 0) return undefined;
|
||||||
|
|
||||||
|
return pool[rng.nextInt(pool.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Total number of layers (start + 11 intermediate + end) */
|
||||||
|
const TOTAL_LAYERS = 13;
|
||||||
|
|
||||||
|
/** Node type for each layer. Undefined layers use combat/elite mix. */
|
||||||
|
const LAYER_TYPE: Partial<Record<number, MapNodeType>> = {
|
||||||
|
0: MapNodeType.Start,
|
||||||
|
3: MapNodeType.Event,
|
||||||
|
6: MapNodeType.Shelter,
|
||||||
|
9: MapNodeType.NPC,
|
||||||
|
12: MapNodeType.Boss,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many nodes each layer should have.
|
||||||
|
* Diamond-ish shape: 1→2→3→4→5→5→5→5→4→3→2→1
|
||||||
|
*/
|
||||||
|
const LAYER_WIDTHS = [1, 2, 3, 4, 5, 5, 5, 5, 5, 4, 3, 2, 1];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random point crawl map with layered directional graph.
|
||||||
|
*
|
||||||
|
* Invariants:
|
||||||
|
* - 13 layers (index 0 = start, index 12 = boss end)
|
||||||
|
* - Layer 3 = all events, layer 6 = shelters, layer 9 = NPCs
|
||||||
|
* - Every node has 1–2 outgoing edges to the next layer
|
||||||
|
* - Every node is reachable from start and can reach the end
|
||||||
|
*
|
||||||
|
* @param seed Random seed for reproducibility
|
||||||
|
*/
|
||||||
|
export function generatePointCrawlMap(seed?: number): PointCrawlMap {
|
||||||
|
const rng = new Mulberry32RNG(seed ?? Date.now());
|
||||||
|
const actualSeed = rng.getSeed();
|
||||||
|
|
||||||
|
const layers: MapLayer[] = [];
|
||||||
|
const nodes = new Map<string, MapNode>();
|
||||||
|
|
||||||
|
// Step 1: create layers and nodes
|
||||||
|
for (let i = 0; i < TOTAL_LAYERS; i++) {
|
||||||
|
const count = LAYER_WIDTHS[i];
|
||||||
|
const layerType = LAYER_TYPE[i];
|
||||||
|
const nodeIds: string[] = [];
|
||||||
|
|
||||||
|
for (let j = 0; j < count; j++) {
|
||||||
|
const id = `node-${i}-${j}`;
|
||||||
|
const type = layerType ?? pickLayerNodeType(i, rng);
|
||||||
|
const encounter = pickEncounterForNode(type, rng);
|
||||||
|
const node: MapNode = {
|
||||||
|
id,
|
||||||
|
layerIndex: i,
|
||||||
|
type,
|
||||||
|
childIds: [],
|
||||||
|
...(encounter ? { encounter: { name: encounter.name, description: encounter.description } } : {}),
|
||||||
|
};
|
||||||
|
nodes.set(id, node);
|
||||||
|
nodeIds.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
layers.push({ index: i, nodeIds });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: generate edges between each pair of consecutive layers
|
||||||
|
for (let i = 0; i < TOTAL_LAYERS - 1; i++) {
|
||||||
|
const sourceIds = layers[i].nodeIds;
|
||||||
|
const targetIds = layers[i + 1].nodeIds;
|
||||||
|
generateLayerEdges(sourceIds, targetIds, nodes, rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { layers, nodes, seed: actualSeed };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks a node type for a general (non-fixed) layer.
|
||||||
|
* Elite nodes appear ~25% of the time, combat for the rest.
|
||||||
|
*/
|
||||||
|
function pickLayerNodeType(_layerIndex: number, rng: RNG): MapNodeType {
|
||||||
|
return rng.nextInt(4) === 0 ? MapNodeType.Elite : MapNodeType.Combat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates edges between two consecutive layers.
|
||||||
|
*
|
||||||
|
* Constraints:
|
||||||
|
* - Each source node gets 1–2 edges to target nodes
|
||||||
|
* - Every target node has at least one incoming edge (no dead ends)
|
||||||
|
*/
|
||||||
|
function generateLayerEdges(
|
||||||
|
sourceIds: string[],
|
||||||
|
targetIds: string[],
|
||||||
|
nodes: Map<string, MapNode>,
|
||||||
|
rng: RNG
|
||||||
|
): void {
|
||||||
|
const sourceBranches = new Map<string, number>(); // id → current outgoing count
|
||||||
|
const targetIncoming = new Map<string, number>(); // id → current incoming count
|
||||||
|
for (const id of sourceIds) sourceBranches.set(id, 0);
|
||||||
|
for (const id of targetIds) targetIncoming.set(id, 0);
|
||||||
|
|
||||||
|
// --- Pass 1: give each source 1–2 targets, prioritising uncovered targets ---
|
||||||
|
const uncovered = new Set(targetIds);
|
||||||
|
|
||||||
|
for (const srcId of sourceIds) {
|
||||||
|
const branches = rng.nextInt(2) + 1; // 1 or 2
|
||||||
|
|
||||||
|
for (let b = 0; b < branches; b++) {
|
||||||
|
if (uncovered.size > 0) {
|
||||||
|
// Pick a random uncovered target
|
||||||
|
const arr = Array.from(uncovered);
|
||||||
|
const idx = rng.nextInt(arr.length);
|
||||||
|
const tgtId = arr[idx];
|
||||||
|
nodes.get(srcId)!.childIds.push(tgtId);
|
||||||
|
sourceBranches.set(srcId, sourceBranches.get(srcId)! + 1);
|
||||||
|
targetIncoming.set(tgtId, targetIncoming.get(tgtId)! + 1);
|
||||||
|
uncovered.delete(tgtId);
|
||||||
|
} else if (sourceBranches.get(srcId)! < 2) {
|
||||||
|
// All targets covered; pick any random target
|
||||||
|
const tgtId = targetIds[rng.nextInt(targetIds.length)];
|
||||||
|
nodes.get(srcId)!.childIds.push(tgtId);
|
||||||
|
sourceBranches.set(srcId, sourceBranches.get(srcId)! + 1);
|
||||||
|
targetIncoming.set(tgtId, targetIncoming.get(tgtId)! + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pass 2: cover any remaining uncovered targets ---
|
||||||
|
for (const tgtId of uncovered) {
|
||||||
|
// Find a source that still has room (< 2 branches)
|
||||||
|
const available = sourceIds.filter(id => sourceBranches.get(id)! < 2);
|
||||||
|
if (available.length > 0) {
|
||||||
|
const srcId = available[rng.nextInt(available.length)];
|
||||||
|
nodes.get(srcId)!.childIds.push(tgtId);
|
||||||
|
sourceBranches.set(srcId, sourceBranches.get(srcId)! + 1);
|
||||||
|
targetIncoming.set(tgtId, targetIncoming.get(tgtId)! + 1);
|
||||||
|
} else {
|
||||||
|
// All sources are at 2 branches; force-add to a random source
|
||||||
|
const srcId = sourceIds[rng.nextInt(sourceIds.length)];
|
||||||
|
nodes.get(srcId)!.childIds.push(tgtId);
|
||||||
|
targetIncoming.set(tgtId, targetIncoming.get(tgtId)! + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Query helpers --
|
||||||
|
|
||||||
|
/** Returns the node with the given ID, or undefined. */
|
||||||
|
export function getNode(map: PointCrawlMap, nodeId: string): MapNode | undefined {
|
||||||
|
return map.nodes.get(nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns child nodes of the given node. */
|
||||||
|
export function getChildren(map: PointCrawlMap, node: MapNode): MapNode[] {
|
||||||
|
return node.childIds
|
||||||
|
.map(id => map.nodes.get(id))
|
||||||
|
.filter((n): n is MapNode => n !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns parent nodes of the given node (reverse lookup). */
|
||||||
|
export function getParents(map: PointCrawlMap, node: MapNode): MapNode[] {
|
||||||
|
const parents: MapNode[] = [];
|
||||||
|
const parentLayer = map.layers[node.layerIndex - 1];
|
||||||
|
if (!parentLayer) return parents;
|
||||||
|
|
||||||
|
for (const parentId of parentLayer.nodeIds) {
|
||||||
|
const parent = map.nodes.get(parentId);
|
||||||
|
if (parent?.childIds.includes(node.id)) {
|
||||||
|
parents.push(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if there is a directed path from `fromId` to `toId`.
|
||||||
|
*/
|
||||||
|
export function hasPath(map: PointCrawlMap, fromId: string, toId: string): boolean {
|
||||||
|
const visited = new Set<string>();
|
||||||
|
const stack = [fromId];
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const current = stack.pop()!;
|
||||||
|
if (current === toId) return true;
|
||||||
|
if (visited.has(current)) continue;
|
||||||
|
visited.add(current);
|
||||||
|
|
||||||
|
const node = map.nodes.get(current);
|
||||||
|
if (node) {
|
||||||
|
for (const childId of node.childIds) {
|
||||||
|
if (!visited.has(childId)) stack.push(childId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all directed paths from `fromId` to `toId`.
|
||||||
|
* Returns arrays of node IDs representing each path.
|
||||||
|
* Beware: can be exponential in large maps.
|
||||||
|
*/
|
||||||
|
export function findAllPaths(map: PointCrawlMap, fromId: string, toId: string): string[][] {
|
||||||
|
const paths: string[][] = [];
|
||||||
|
const dfs = (current: string, path: string[]) => {
|
||||||
|
if (current === toId) {
|
||||||
|
paths.push([...path, current]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const node = map.nodes.get(current);
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
path.push(current);
|
||||||
|
for (const childId of node.childIds) {
|
||||||
|
dfs(childId, path);
|
||||||
|
}
|
||||||
|
path.pop();
|
||||||
|
};
|
||||||
|
|
||||||
|
dfs(fromId, []);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export { MapNodeType } from './types';
|
||||||
|
export type { MapNode, MapLayer, PointCrawlMap } from './types';
|
||||||
|
|
||||||
|
export { generatePointCrawlMap } from './generator';
|
||||||
|
export { getNode, getChildren, getParents, hasPath, findAllPaths } from './generator';
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/**
|
||||||
|
* Types of nodes that can appear on the point crawl map.
|
||||||
|
*/
|
||||||
|
export enum MapNodeType {
|
||||||
|
Start = 'start',
|
||||||
|
Combat = 'combat',
|
||||||
|
Event = 'event',
|
||||||
|
Elite = 'elite',
|
||||||
|
Shelter = 'shelter',
|
||||||
|
NPC = 'npc',
|
||||||
|
Boss = 'boss',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single node on the map.
|
||||||
|
*/
|
||||||
|
export interface MapNode {
|
||||||
|
/** Unique identifier */
|
||||||
|
id: string;
|
||||||
|
/** Which layer this node belongs to */
|
||||||
|
layerIndex: number;
|
||||||
|
/** Semantic type of the node */
|
||||||
|
type: MapNodeType;
|
||||||
|
/** IDs of nodes in the next layer this node connects to */
|
||||||
|
childIds: string[];
|
||||||
|
/** Encounter data assigned to this node (from encounter CSV) */
|
||||||
|
encounter?: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A horizontal layer of nodes at the same progression stage.
|
||||||
|
*/
|
||||||
|
export interface MapLayer {
|
||||||
|
/** Layer index (0 = start, last = end) */
|
||||||
|
index: number;
|
||||||
|
/** Ordered IDs of nodes in this layer */
|
||||||
|
nodeIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A fully generated point crawl map.
|
||||||
|
*/
|
||||||
|
export interface PointCrawlMap {
|
||||||
|
/** Layers from start to end */
|
||||||
|
layers: MapLayer[];
|
||||||
|
/** All nodes keyed by ID */
|
||||||
|
nodes: Map<string, MapNode>;
|
||||||
|
/** RNG seed used for generation (for reproducibility) */
|
||||||
|
seed: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { generatePointCrawlMap } from '@/samples/slay-the-spire-like/map/generator';
|
||||||
|
import { MapNodeType } from '@/samples/slay-the-spire-like/map/types';
|
||||||
|
|
||||||
|
describe('generatePointCrawlMap', () => {
|
||||||
|
it('should generate a map with 13 layers', () => {
|
||||||
|
const map = generatePointCrawlMap(123);
|
||||||
|
expect(map.layers.length).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct fixed layer types', () => {
|
||||||
|
const map = generatePointCrawlMap(123);
|
||||||
|
const startNode = map.nodes.get('node-0-0');
|
||||||
|
const bossNode = map.nodes.get('node-12-0');
|
||||||
|
|
||||||
|
expect(startNode?.type).toBe(MapNodeType.Start);
|
||||||
|
expect(bossNode?.type).toBe(MapNodeType.Boss);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should assign encounters to nodes based on encounterDesert.csv', () => {
|
||||||
|
const map = generatePointCrawlMap(456);
|
||||||
|
|
||||||
|
// Check that nodes have encounters assigned
|
||||||
|
let combatWithEncounter = 0;
|
||||||
|
let eliteWithEncounter = 0;
|
||||||
|
let bossWithEncounter = 0;
|
||||||
|
let eventWithEncounter = 0;
|
||||||
|
let npcWithEncounter = 0;
|
||||||
|
let shelterWithEncounter = 0;
|
||||||
|
|
||||||
|
for (const node of map.nodes.values()) {
|
||||||
|
if (node.type === MapNodeType.Combat && node.encounter) {
|
||||||
|
combatWithEncounter++;
|
||||||
|
expect(node.encounter.name).toBeTruthy();
|
||||||
|
expect(node.encounter.description).toBeTruthy();
|
||||||
|
}
|
||||||
|
if (node.type === MapNodeType.Elite && node.encounter) {
|
||||||
|
eliteWithEncounter++;
|
||||||
|
expect(node.encounter.name).toBeTruthy();
|
||||||
|
expect(node.encounter.description).toBeTruthy();
|
||||||
|
}
|
||||||
|
if (node.type === MapNodeType.Boss && node.encounter) {
|
||||||
|
bossWithEncounter++;
|
||||||
|
expect(node.encounter.name).toBeTruthy();
|
||||||
|
expect(node.encounter.description).toBeTruthy();
|
||||||
|
}
|
||||||
|
if (node.type === MapNodeType.Event && node.encounter) {
|
||||||
|
eventWithEncounter++;
|
||||||
|
expect(node.encounter.name).toBeTruthy();
|
||||||
|
expect(node.encounter.description).toBeTruthy();
|
||||||
|
}
|
||||||
|
if (node.type === MapNodeType.NPC && node.encounter) {
|
||||||
|
npcWithEncounter++;
|
||||||
|
expect(node.encounter.name).toBeTruthy();
|
||||||
|
expect(node.encounter.description).toBeTruthy();
|
||||||
|
}
|
||||||
|
if (node.type === MapNodeType.Shelter && node.encounter) {
|
||||||
|
shelterWithEncounter++;
|
||||||
|
expect(node.encounter.name).toBeTruthy();
|
||||||
|
expect(node.encounter.description).toBeTruthy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have assigned at least some encounters
|
||||||
|
const totalWithEncounters =
|
||||||
|
combatWithEncounter +
|
||||||
|
eliteWithEncounter +
|
||||||
|
bossWithEncounter +
|
||||||
|
eventWithEncounter +
|
||||||
|
npcWithEncounter +
|
||||||
|
shelterWithEncounter;
|
||||||
|
|
||||||
|
expect(totalWithEncounters).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use correct encounter types for each node type', () => {
|
||||||
|
const map = generatePointCrawlMap(789);
|
||||||
|
|
||||||
|
for (const node of map.nodes.values()) {
|
||||||
|
if (node.encounter) {
|
||||||
|
// Encounter should match node type conceptually
|
||||||
|
// Combat nodes should have enemy encounters, elites should have elite encounters, etc.
|
||||||
|
if (node.type === MapNodeType.Boss) {
|
||||||
|
expect(node.encounter.description).toContain('Boss');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue