|
|
@@ -1,21 +1,21 @@
|
|
|
|
import { Mulberry32RNG, type RNG } from '@/utils/rng';
|
|
|
|
import { Mulberry32RNG, type RNG } from '@/utils/rng';
|
|
|
|
import encounterDesertCsv, { type EncounterDesert } from '../data/encounterDesert.csv';
|
|
|
|
import encounterDesertCsv, { type EncounterDesert } from '../data/encounterDesert.csv';
|
|
|
|
import { MapNodeType, MapLayerType } from './types';
|
|
|
|
import { MapNodeType, MapLayerType } from './types';
|
|
|
|
import type { MapLayer, MapNode, PointCrawlMap } from './types';
|
|
|
|
import type { MapLayer, MapNode, PointCrawlMap, MapGenerationConfig } from './types';
|
|
|
|
|
|
|
|
|
|
|
|
/** Cache for parsed encounters by type */
|
|
|
|
/** Pre-indexed encounters by type */
|
|
|
|
const encountersByType = new Map<string, EncounterDesert[]>();
|
|
|
|
const encountersByType = buildEncounterIndex();
|
|
|
|
|
|
|
|
|
|
|
|
function indexEncounters(): void {
|
|
|
|
|
|
|
|
if (encountersByType.size > 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function buildEncounterIndex(): Map<string, EncounterDesert[]> {
|
|
|
|
|
|
|
|
const index = new Map<string, EncounterDesert[]>();
|
|
|
|
for (const encounter of encounterDesertCsv) {
|
|
|
|
for (const encounter of encounterDesertCsv) {
|
|
|
|
const type = encounter.type;
|
|
|
|
const type = encounter.type;
|
|
|
|
if (!encountersByType.has(type)) {
|
|
|
|
if (!index.has(type)) {
|
|
|
|
encountersByType.set(type, []);
|
|
|
|
index.set(type, []);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
encountersByType.get(type)!.push(encounter);
|
|
|
|
index.get(type)!.push(encounter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Map from MapNodeType to encounter type key */
|
|
|
|
/** Map from MapNodeType to encounter type key */
|
|
|
@@ -28,23 +28,17 @@ const NODE_TYPE_TO_ENCOUNTER: Partial<Record<MapNodeType, string>> = {
|
|
|
|
[MapNodeType.Curio]: 'shelter',
|
|
|
|
[MapNodeType.Curio]: 'shelter',
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/** Default map generation configuration */
|
|
|
|
* Picks a random encounter for the given node type.
|
|
|
|
const DEFAULT_CONFIG: MapGenerationConfig = {
|
|
|
|
* Returns undefined if no matching encounter exists.
|
|
|
|
totalLayers: 10,
|
|
|
|
*/
|
|
|
|
wildLayerNodeCount: 3,
|
|
|
|
function pickEncounterForNode(type: MapNodeType, rng: RNG): EncounterDesert | undefined {
|
|
|
|
settlementLayerNodeCount: 4,
|
|
|
|
indexEncounters();
|
|
|
|
wildNodeTypeWeights: {
|
|
|
|
const encounterType = NODE_TYPE_TO_ENCOUNTER[type];
|
|
|
|
[MapNodeType.Minion]: 50,
|
|
|
|
if (!encounterType) return undefined;
|
|
|
|
[MapNodeType.Elite]: 25,
|
|
|
|
|
|
|
|
[MapNodeType.Event]: 25,
|
|
|
|
const pool = encountersByType.get(encounterType);
|
|
|
|
},
|
|
|
|
if (!pool || pool.length === 0) return undefined;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return pool[rng.nextInt(pool.length)];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Total number of layers */
|
|
|
|
|
|
|
|
const TOTAL_LAYERS = 10;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Layer structure definition.
|
|
|
|
* Layer structure definition.
|
|
|
@@ -63,6 +57,35 @@ const LAYER_STRUCTURE: Array<{ layerType: MapLayerType | 'start' | 'end'; count:
|
|
|
|
{ layerType: 'end', count: 1 },
|
|
|
|
{ layerType: 'end', count: 1 },
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Fisher-Yates shuffle algorithm for unbiased random permutation.
|
|
|
|
|
|
|
|
* Mutates the array in place and returns it.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
function fisherYatesShuffle<T>(array: T[], rng: RNG): T[] {
|
|
|
|
|
|
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
|
|
|
|
|
|
const j = rng.nextInt(i + 1);
|
|
|
|
|
|
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return array;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
|
|
* Picks a random encounter for the given node type.
|
|
|
|
|
|
|
|
* Returns undefined if no matching encounter exists.
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
function pickEncounterForNode(type: MapNodeType, rng: RNG): EncounterDesert | undefined {
|
|
|
|
|
|
|
|
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 */
|
|
|
|
|
|
|
|
const TOTAL_LAYERS = 10;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Generates a random point crawl map with layered directional graph.
|
|
|
|
* Generates a random point crawl map with layered directional graph.
|
|
|
|
*
|
|
|
|
*
|
|
|
@@ -86,6 +109,7 @@ export function generatePointCrawlMap(seed?: number): PointCrawlMap {
|
|
|
|
for (let i = 0; i < TOTAL_LAYERS; i++) {
|
|
|
|
for (let i = 0; i < TOTAL_LAYERS; i++) {
|
|
|
|
const structure = LAYER_STRUCTURE[i];
|
|
|
|
const structure = LAYER_STRUCTURE[i];
|
|
|
|
const nodeIds: string[] = [];
|
|
|
|
const nodeIds: string[] = [];
|
|
|
|
|
|
|
|
const layerNodes: MapNode[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (let j = 0; j < structure.count; j++) {
|
|
|
|
for (let j = 0; j < structure.count; j++) {
|
|
|
|
const id = `node-${i}-${j}`;
|
|
|
|
const id = `node-${i}-${j}`;
|
|
|
@@ -100,19 +124,32 @@ export function generatePointCrawlMap(seed?: number): PointCrawlMap {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
nodes.set(id, node);
|
|
|
|
nodes.set(id, node);
|
|
|
|
nodeIds.push(id);
|
|
|
|
nodeIds.push(id);
|
|
|
|
|
|
|
|
layerNodes.push(node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
layers.push({ index: i, nodeIds, layerType: structure.layerType });
|
|
|
|
layers.push({ index: i, nodeIds, layerType: structure.layerType, nodes: layerNodes });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Step 2: generate edges between consecutive layers
|
|
|
|
// Step 2: generate edges between consecutive layers
|
|
|
|
|
|
|
|
const parentIndex = new Map<string, string[]>();
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < TOTAL_LAYERS - 1; i++) {
|
|
|
|
for (let i = 0; i < TOTAL_LAYERS - 1; i++) {
|
|
|
|
const sourceLayer = layers[i];
|
|
|
|
const sourceLayer = layers[i];
|
|
|
|
const targetLayer = layers[i + 1];
|
|
|
|
const targetLayer = layers[i + 1];
|
|
|
|
generateLayerEdges(sourceLayer, targetLayer, nodes, rng);
|
|
|
|
generateLayerEdges(sourceLayer, targetLayer, nodes, rng);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Build reverse index: for each target node, record its parents
|
|
|
|
|
|
|
|
for (const srcNode of sourceLayer.nodes) {
|
|
|
|
|
|
|
|
for (const childId of srcNode.childIds) {
|
|
|
|
|
|
|
|
if (!parentIndex.has(childId)) {
|
|
|
|
|
|
|
|
parentIndex.set(childId, []);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
parentIndex.get(childId)!.push(srcNode.id);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return { layers, nodes, seed: actualSeed };
|
|
|
|
return { layers, nodes, seed: actualSeed, parentIndex };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
@@ -140,35 +177,35 @@ function resolveNodeType(
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Picks a random type for a wild node.
|
|
|
|
* Picks a random type for a wild node based on configured weights.
|
|
|
|
* minion: 50%, elite: 25%, event: 25%
|
|
|
|
* Default: minion: 50%, elite: 25%, event: 25%
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
function pickWildNodeType(rng: RNG): MapNodeType {
|
|
|
|
function pickWildNodeType(rng: RNG): MapNodeType {
|
|
|
|
const roll = rng.nextInt(4);
|
|
|
|
const weights = DEFAULT_CONFIG.wildNodeTypeWeights;
|
|
|
|
if (roll === 0) return MapNodeType.Elite;
|
|
|
|
const roll = rng.nextInt(100);
|
|
|
|
if (roll === 1) return MapNodeType.Event;
|
|
|
|
|
|
|
|
return MapNodeType.Minion;
|
|
|
|
if (roll < weights[MapNodeType.Minion]) return MapNodeType.Minion;
|
|
|
|
|
|
|
|
if (roll < weights[MapNodeType.Minion] + weights[MapNodeType.Elite]) return MapNodeType.Elite;
|
|
|
|
|
|
|
|
return MapNodeType.Event;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* Assigns settlement node types ensuring at least 1 of each: camp, shop, curio.
|
|
|
|
* Assigns settlement node types ensuring at least 1 of each: camp, shop, curio.
|
|
|
|
* The 4th node is randomly chosen from the three.
|
|
|
|
* The 4th node is randomly chosen from the three.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
function assignSettlementTypes(nodeIds: string[], nodes: Map<string, MapNode>, rng: RNG): void {
|
|
|
|
function assignSettlementTypes(nodeIds: string[], nodes: MapNode[], rng: RNG): void {
|
|
|
|
// Shuffle node order to randomize which position gets which type
|
|
|
|
// Shuffle node order to randomize which position gets which type
|
|
|
|
const shuffledIndices = [0, 1, 2, 3].sort(() => rng.next() - 0.5);
|
|
|
|
const shuffledIndices = fisherYatesShuffle([0, 1, 2, 3], rng);
|
|
|
|
|
|
|
|
|
|
|
|
// Assign camp, shop, curio to first 3 shuffled positions
|
|
|
|
// Assign camp, shop, curio to first 3 shuffled positions
|
|
|
|
const requiredTypes = [MapNodeType.Camp, MapNodeType.Shop, MapNodeType.Curio];
|
|
|
|
const requiredTypes = [MapNodeType.Camp, MapNodeType.Shop, MapNodeType.Curio];
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
const node = nodes.get(nodeIds[shuffledIndices[i]])!;
|
|
|
|
nodes[shuffledIndices[i]].type = requiredTypes[i];
|
|
|
|
node.type = requiredTypes[i];
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Assign random type to 4th position
|
|
|
|
// Assign random type to 4th position
|
|
|
|
const randomType = requiredTypes[rng.nextInt(3)];
|
|
|
|
const randomType = requiredTypes[rng.nextInt(3)];
|
|
|
|
const node = nodes.get(nodeIds[shuffledIndices[3]])!;
|
|
|
|
nodes[shuffledIndices[3]].type = randomType;
|
|
|
|
node.type = randomType;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
/**
|
|
|
@@ -182,22 +219,22 @@ function generateLayerEdges(
|
|
|
|
): void {
|
|
|
|
): void {
|
|
|
|
// Assign settlement types when creating settlement layer
|
|
|
|
// Assign settlement types when creating settlement layer
|
|
|
|
if (targetLayer.layerType === MapLayerType.Settlement) {
|
|
|
|
if (targetLayer.layerType === MapLayerType.Settlement) {
|
|
|
|
assignSettlementTypes(targetLayer.nodeIds, nodes, rng);
|
|
|
|
assignSettlementTypes(targetLayer.nodeIds, targetLayer.nodes, rng);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sourceType = sourceLayer.layerType;
|
|
|
|
const sourceType = sourceLayer.layerType;
|
|
|
|
const targetType = targetLayer.layerType;
|
|
|
|
const targetType = targetLayer.layerType;
|
|
|
|
|
|
|
|
|
|
|
|
if (sourceType === 'start' && targetType === MapLayerType.Wild) {
|
|
|
|
if (sourceType === 'start' && targetType === MapLayerType.Wild) {
|
|
|
|
connectStartToWild(sourceLayer, targetLayer, nodes);
|
|
|
|
connectStartToWild(sourceLayer, targetLayer);
|
|
|
|
} else if (sourceType === MapLayerType.Wild && targetType === MapLayerType.Wild) {
|
|
|
|
} else if (sourceType === MapLayerType.Wild && targetType === MapLayerType.Wild) {
|
|
|
|
connectWildToWild(sourceLayer, targetLayer, nodes, rng);
|
|
|
|
connectWildToWild(sourceLayer, targetLayer);
|
|
|
|
} else if (sourceType === MapLayerType.Wild && targetType === MapLayerType.Settlement) {
|
|
|
|
} else if (sourceType === MapLayerType.Wild && targetType === MapLayerType.Settlement) {
|
|
|
|
connectWildToSettlement(sourceLayer, targetLayer, nodes, rng);
|
|
|
|
connectWildToSettlement(sourceLayer, targetLayer, rng);
|
|
|
|
} else if (sourceType === MapLayerType.Settlement && targetType === MapLayerType.Wild) {
|
|
|
|
} else if (sourceType === MapLayerType.Settlement && targetType === MapLayerType.Wild) {
|
|
|
|
connectSettlementToWild(sourceLayer, targetLayer, nodes, rng);
|
|
|
|
connectSettlementToWild(sourceLayer, targetLayer, rng);
|
|
|
|
} else if (sourceType === MapLayerType.Wild && targetType === 'end') {
|
|
|
|
} else if (sourceType === MapLayerType.Wild && targetType === 'end') {
|
|
|
|
connectWildToEnd(sourceLayer, targetLayer, nodes);
|
|
|
|
connectWildToEnd(sourceLayer, targetLayer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -206,10 +243,9 @@ function generateLayerEdges(
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
function connectStartToWild(
|
|
|
|
function connectStartToWild(
|
|
|
|
startLayer: MapLayer,
|
|
|
|
startLayer: MapLayer,
|
|
|
|
wildLayer: MapLayer,
|
|
|
|
wildLayer: MapLayer
|
|
|
|
nodes: Map<string, MapNode>
|
|
|
|
|
|
|
|
): void {
|
|
|
|
): void {
|
|
|
|
const startNode = nodes.get(startLayer.nodeIds[0])!;
|
|
|
|
const startNode = startLayer.nodes[0];
|
|
|
|
startNode.childIds = [...wildLayer.nodeIds];
|
|
|
|
startNode.childIds = [...wildLayer.nodeIds];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -219,15 +255,12 @@ function connectStartToWild(
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
function connectWildToWild(
|
|
|
|
function connectWildToWild(
|
|
|
|
sourceLayer: MapLayer,
|
|
|
|
sourceLayer: MapLayer,
|
|
|
|
targetLayer: MapLayer,
|
|
|
|
targetLayer: MapLayer
|
|
|
|
nodes: Map<string, MapNode>,
|
|
|
|
|
|
|
|
_rng: RNG
|
|
|
|
|
|
|
|
): void {
|
|
|
|
): void {
|
|
|
|
// Direct 1-to-1 mapping: wild[i] → wild[i]
|
|
|
|
// Direct 1-to-1 mapping: wild[i] → wild[i]
|
|
|
|
// This guarantees no crossings since order is preserved
|
|
|
|
// This guarantees no crossings since order is preserved
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
for (let i = 0; i < sourceLayer.nodes.length; i++) {
|
|
|
|
const srcNode = nodes.get(sourceLayer.nodeIds[i])!;
|
|
|
|
sourceLayer.nodes[i].childIds = [targetLayer.nodeIds[i]];
|
|
|
|
srcNode.childIds = [targetLayer.nodeIds[i]];
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -239,53 +272,25 @@ function connectWildToWild(
|
|
|
|
function connectWildToSettlement(
|
|
|
|
function connectWildToSettlement(
|
|
|
|
wildLayer: MapLayer,
|
|
|
|
wildLayer: MapLayer,
|
|
|
|
settlementLayer: MapLayer,
|
|
|
|
settlementLayer: MapLayer,
|
|
|
|
nodes: Map<string, MapNode>,
|
|
|
|
|
|
|
|
rng: RNG
|
|
|
|
rng: RNG
|
|
|
|
): void {
|
|
|
|
): void {
|
|
|
|
// Strategy: create a mapping where each wild gets exactly 2 settlements
|
|
|
|
// Non-crossing connection pattern: each wild connects to 2 adjacent settlements.
|
|
|
|
// and all 4 settlements are covered
|
|
|
|
// Base pattern: w[0]→{s[0],s[1]}, w[1]→{s[1],s[2]}, w[2]→{s[2],s[3]}
|
|
|
|
// Example pattern: wild[0]→{s0,s1}, wild[1]→{s1,s2}, wild[2]→{s2,s3}
|
|
|
|
// This creates a "chain" where middle settlements are shared.
|
|
|
|
// But we want randomness, so:
|
|
|
|
//
|
|
|
|
// 1. Shuffle settlements
|
|
|
|
// Variation: randomly flip to reverse pattern w[0]→{s[2],s[3]}, w[1]→{s[1],s[2]}, w[2]→{s[0],s[1]}
|
|
|
|
// 2. Assign first 3 settlements to wilds 0,1,2 (guarantee coverage)
|
|
|
|
// Both patterns guarantee no crossings.
|
|
|
|
// 3. 4th settlement goes to a random wild
|
|
|
|
|
|
|
|
// 4. Each wild picks one more from remaining available
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const settlementOrder = [0, 1, 2, 3].sort(() => rng.next() - 0.5);
|
|
|
|
const reverse = rng.next() < 0.5;
|
|
|
|
|
|
|
|
|
|
|
|
// Initial assignment: each wild gets 1 unique settlement
|
|
|
|
if (reverse) {
|
|
|
|
const assignments: Set<number>[] = [
|
|
|
|
wildLayer.nodes[0].childIds = [settlementLayer.nodeIds[2], settlementLayer.nodeIds[3]];
|
|
|
|
new Set([settlementOrder[0]]),
|
|
|
|
wildLayer.nodes[1].childIds = [settlementLayer.nodeIds[1], settlementLayer.nodeIds[2]];
|
|
|
|
new Set([settlementOrder[1]]),
|
|
|
|
wildLayer.nodes[2].childIds = [settlementLayer.nodeIds[0], settlementLayer.nodeIds[1]];
|
|
|
|
new Set([settlementOrder[2]]),
|
|
|
|
} else {
|
|
|
|
];
|
|
|
|
wildLayer.nodes[0].childIds = [settlementLayer.nodeIds[0], settlementLayer.nodeIds[1]];
|
|
|
|
|
|
|
|
wildLayer.nodes[1].childIds = [settlementLayer.nodeIds[1], settlementLayer.nodeIds[2]];
|
|
|
|
// 4th settlement goes to a random wild
|
|
|
|
wildLayer.nodes[2].childIds = [settlementLayer.nodeIds[2], settlementLayer.nodeIds[3]];
|
|
|
|
const wildForFourth = rng.nextInt(3);
|
|
|
|
|
|
|
|
assignments[wildForFourth].add(settlementOrder[3]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Now each wild needs exactly 2 settlements
|
|
|
|
|
|
|
|
// Find which wilds still need 1 more
|
|
|
|
|
|
|
|
const needMore: number[] = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
|
|
|
if (assignments[i].size < 2) {
|
|
|
|
|
|
|
|
needMore.push(i);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// These wilds pick from settlements that already have coverage
|
|
|
|
|
|
|
|
// to create convergence (multiple wilds → same settlement)
|
|
|
|
|
|
|
|
for (const wildIdx of needMore) {
|
|
|
|
|
|
|
|
// Pick a random settlement (excluding the one already assigned)
|
|
|
|
|
|
|
|
const available = [0, 1, 2, 3].filter(s => !assignments[wildIdx].has(s));
|
|
|
|
|
|
|
|
const pick = available[rng.nextInt(available.length)];
|
|
|
|
|
|
|
|
assignments[wildIdx].add(pick);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Assign childIds
|
|
|
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
|
|
|
const srcNode = nodes.get(wildLayer.nodeIds[i])!;
|
|
|
|
|
|
|
|
srcNode.childIds = [...assignments[i]].map(idx => settlementLayer.nodeIds[idx]);
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -294,33 +299,31 @@ function connectWildToSettlement(
|
|
|
|
* - First and last settlement connect to 1 wild each
|
|
|
|
* - First and last settlement connect to 1 wild each
|
|
|
|
* - Middle two settlements connect to 2 wilds each
|
|
|
|
* - Middle two settlements connect to 2 wilds each
|
|
|
|
* Total: 1 + 2 + 2 + 1 = 6 edges, 3 wild nodes to cover
|
|
|
|
* Total: 1 + 2 + 2 + 1 = 6 edges, 3 wild nodes to cover
|
|
|
|
*
|
|
|
|
*
|
|
|
|
* Uses a non-crossing pattern: settlements and wilds are connected
|
|
|
|
* Uses a non-crossing pattern: settlements and wilds are connected
|
|
|
|
* in order to avoid edge crossings.
|
|
|
|
* in order to avoid edge crossings.
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
function connectSettlementToWild(
|
|
|
|
function connectSettlementToWild(
|
|
|
|
settlementLayer: MapLayer,
|
|
|
|
settlementLayer: MapLayer,
|
|
|
|
wildLayer: MapLayer,
|
|
|
|
wildLayer: MapLayer,
|
|
|
|
nodes: Map<string, MapNode>,
|
|
|
|
|
|
|
|
rng: RNG
|
|
|
|
rng: RNG
|
|
|
|
): void {
|
|
|
|
): void {
|
|
|
|
// Non-crossing pattern with circular shift option
|
|
|
|
// Non-crossing pattern: s0→w0, s1→w0,w1, s2→w1,w2, s3→w2
|
|
|
|
// Base pattern: s0→w0, s1→w0,w1, s2→w1,w2, s3→w2
|
|
|
|
// Variation: randomly flip to reverse pattern s0→w2, s1→w1,w2, s2→w0,w1, s3→w0
|
|
|
|
// Apply circular shift to wilds for variety
|
|
|
|
// Both patterns guarantee no crossings.
|
|
|
|
|
|
|
|
|
|
|
|
const shift = rng.nextInt(3);
|
|
|
|
|
|
|
|
const wildIdx = (i: number) => (i + shift) % 3;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const settlementAssignments: number[][] = [
|
|
|
|
const reverse = rng.next() < 0.5;
|
|
|
|
[wildIdx(0)],
|
|
|
|
|
|
|
|
[wildIdx(0), wildIdx(1)],
|
|
|
|
|
|
|
|
[wildIdx(1), wildIdx(2)],
|
|
|
|
|
|
|
|
[wildIdx(2)],
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
if (reverse) {
|
|
|
|
const srcNode = nodes.get(settlementLayer.nodeIds[i])!;
|
|
|
|
settlementLayer.nodes[0].childIds = [wildLayer.nodeIds[2]];
|
|
|
|
srcNode.childIds = settlementAssignments[i].map(idx => wildLayer.nodeIds[idx]);
|
|
|
|
settlementLayer.nodes[1].childIds = [wildLayer.nodeIds[1], wildLayer.nodeIds[2]];
|
|
|
|
|
|
|
|
settlementLayer.nodes[2].childIds = [wildLayer.nodeIds[0], wildLayer.nodeIds[1]];
|
|
|
|
|
|
|
|
settlementLayer.nodes[3].childIds = [wildLayer.nodeIds[0]];
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
settlementLayer.nodes[0].childIds = [wildLayer.nodeIds[0]];
|
|
|
|
|
|
|
|
settlementLayer.nodes[1].childIds = [wildLayer.nodeIds[0], wildLayer.nodeIds[1]];
|
|
|
|
|
|
|
|
settlementLayer.nodes[2].childIds = [wildLayer.nodeIds[1], wildLayer.nodeIds[2]];
|
|
|
|
|
|
|
|
settlementLayer.nodes[3].childIds = [wildLayer.nodeIds[2]];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -329,13 +332,11 @@ function connectSettlementToWild(
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
function connectWildToEnd(
|
|
|
|
function connectWildToEnd(
|
|
|
|
wildLayer: MapLayer,
|
|
|
|
wildLayer: MapLayer,
|
|
|
|
endLayer: MapLayer,
|
|
|
|
endLayer: MapLayer
|
|
|
|
nodes: Map<string, MapNode>
|
|
|
|
|
|
|
|
): void {
|
|
|
|
): void {
|
|
|
|
const endNode = nodes.get(endLayer.nodeIds[0])!;
|
|
|
|
const endNode = endLayer.nodes[0];
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
for (let i = 0; i < wildLayer.nodes.length; i++) {
|
|
|
|
const srcNode = nodes.get(wildLayer.nodeIds[i])!;
|
|
|
|
wildLayer.nodes[i].childIds = [endNode.id];
|
|
|
|
srcNode.childIds = [endNode.id];
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@@ -355,6 +356,17 @@ export function getChildren(map: PointCrawlMap, node: MapNode): MapNode[] {
|
|
|
|
|
|
|
|
|
|
|
|
/** Returns parent nodes of the given node (reverse lookup). */
|
|
|
|
/** Returns parent nodes of the given node (reverse lookup). */
|
|
|
|
export function getParents(map: PointCrawlMap, node: MapNode): MapNode[] {
|
|
|
|
export function getParents(map: PointCrawlMap, node: MapNode): MapNode[] {
|
|
|
|
|
|
|
|
// Use pre-built reverse index if available
|
|
|
|
|
|
|
|
if (map.parentIndex) {
|
|
|
|
|
|
|
|
const parentIds = map.parentIndex.get(node.id);
|
|
|
|
|
|
|
|
if (!parentIds) return [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return parentIds
|
|
|
|
|
|
|
|
.map(id => map.nodes.get(id))
|
|
|
|
|
|
|
|
.filter((n): n is MapNode => n !== undefined);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fallback: scan parent layer (legacy support)
|
|
|
|
const parents: MapNode[] = [];
|
|
|
|
const parents: MapNode[] = [];
|
|
|
|
const parentLayer = map.layers[node.layerIndex - 1];
|
|
|
|
const parentLayer = map.layers[node.layerIndex - 1];
|
|
|
|
if (!parentLayer) return parents;
|
|
|
|
if (!parentLayer) return parents;
|
|
|
|