@its-not-rocket-science/ananke 0.1.53 → 0.1.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/src/terrain-bridge.d.ts +161 -0
- package/dist/src/terrain-bridge.js +322 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.54] — 2026-03-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **PA-5 — Campaign ↔ Tactical Terrain Bridge (complete):**
|
|
14
|
+
- `src/terrain-bridge.ts` (new): maps campaign hex tiles to tactical battlefield parameters consumable by `KernelContext`, and merges tactical battle results back into `CampaignState`.
|
|
15
|
+
- `extractTerrainParams(hexType)` → deterministic 10×8-cell (100 m × 80 m) battlefield with `TerrainGrid`, `ObstacleGrid`, `ElevationGrid`, `SlopeGrid`, and `CoverSegment[]` for all 8 hex types: `plains`, `forest`, `hills`, `marsh`, `urban`, `mountain`, `river_crossing`, `coastal`.
|
|
16
|
+
- `generateBattleSite(ctx)` → full `BattleTerrainParams` including `EntryVector[]` — attacker/defender spawn positions (y=5 m south, y=75 m north) with `facingY` direction.
|
|
17
|
+
- `mergeBattleOutcome(campaign, outcome)` → merges post-battle `WorldState` into `CampaignState`: removes `injury.dead` entities, copies post-battle `injury`/`condition` onto survivors, transfers looted weapons/items from captured entities to winner's inventory, advances `worldTime_s`, appends a log entry.
|
|
18
|
+
- Exports: `CampaignHexType`, `EntryVector`, `BattleTerrainParams`, `BattleSiteContext`, `BattleOutcome`; field constants `FIELD_WIDTH_Sm`, `FIELD_HEIGHT_Sm`, `CELL_SIZE_Sm`, `GRID_COLS`, `GRID_ROWS`.
|
|
19
|
+
- `"./terrain-bridge"` subpath export added to `package.json`.
|
|
20
|
+
- 67 new tests (185 test files, 5,397 tests total). Coverage: 97.05% stmt, 87.88% branch, 95.75% func, 97.05% lines. Build: clean.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
9
24
|
## [0.1.53] — 2026-03-28
|
|
10
25
|
|
|
11
26
|
### Added
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { type I32 } from "./units.js";
|
|
2
|
+
import { type TerrainGrid, type ObstacleGrid, type ElevationGrid, type SlopeGrid, type SurfaceType } from "./sim/terrain.js";
|
|
3
|
+
import { type CoverSegment } from "./sim/cover.js";
|
|
4
|
+
import type { CampaignState } from "./campaign.js";
|
|
5
|
+
import type { WorldState } from "./sim/world.js";
|
|
6
|
+
/** Battlefield width [SCALE.m]. 100 m. */
|
|
7
|
+
export declare const FIELD_WIDTH_Sm: I32;
|
|
8
|
+
/** Battlefield depth [SCALE.m]. 80 m. */
|
|
9
|
+
export declare const FIELD_HEIGHT_Sm: I32;
|
|
10
|
+
/** Terrain cell size [SCALE.m]. 10 m per cell → 10 × 8 grid. */
|
|
11
|
+
export declare const CELL_SIZE_Sm: I32;
|
|
12
|
+
/** Number of grid columns (field width / cell size). */
|
|
13
|
+
export declare const GRID_COLS = 10;
|
|
14
|
+
/** Number of grid rows (field height / cell size). */
|
|
15
|
+
export declare const GRID_ROWS = 8;
|
|
16
|
+
/** Attacker spawn y [SCALE.m] — 5 m from the south (y=0) edge. */
|
|
17
|
+
export declare const ATTACKER_SPAWN_Y_Sm: I32;
|
|
18
|
+
/** Defender spawn y [SCALE.m] — 5 m from the north edge. */
|
|
19
|
+
export declare const DEFENDER_SPAWN_Y_Sm: I32;
|
|
20
|
+
/**
|
|
21
|
+
* Campaign map tile type.
|
|
22
|
+
*
|
|
23
|
+
* Each hex type produces a distinct battlefield layout — surface type, cover
|
|
24
|
+
* density, elevation profile, and obstacle placement.
|
|
25
|
+
*/
|
|
26
|
+
export type CampaignHexType = "plains" | "forest" | "hills" | "marsh" | "urban" | "mountain" | "river_crossing" | "coastal";
|
|
27
|
+
/**
|
|
28
|
+
* Initial spawn position and facing for one team entering the battlefield.
|
|
29
|
+
*
|
|
30
|
+
* Attackers (south-entry) have `facingY: 1` (moving toward increasing y).
|
|
31
|
+
* Defenders (north-entry) have `facingY: -1`.
|
|
32
|
+
*/
|
|
33
|
+
export interface EntryVector {
|
|
34
|
+
/** Team identifier matching `entity.teamId`. */
|
|
35
|
+
teamId: number;
|
|
36
|
+
/** Spawn x-coordinate [SCALE.m]. */
|
|
37
|
+
x_Sm: number;
|
|
38
|
+
/** Spawn y-coordinate [SCALE.m]. */
|
|
39
|
+
y_Sm: number;
|
|
40
|
+
/**
|
|
41
|
+
* Initial movement direction along the y-axis.
|
|
42
|
+
* `1` = attacker advancing north.
|
|
43
|
+
* `-1` = defender holding or advancing south.
|
|
44
|
+
*/
|
|
45
|
+
facingY: 1 | -1;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Complete tactical battlefield specification derived from a campaign hex encounter.
|
|
49
|
+
*
|
|
50
|
+
* ## Integration
|
|
51
|
+
* ```ts
|
|
52
|
+
* const site = generateBattleSite({ hexType: "forest", ... });
|
|
53
|
+
* const ctx: KernelContext = {
|
|
54
|
+
* tractionCoeff: SURFACE_TRACTION[site.dominantSurface],
|
|
55
|
+
* cellSize_m: site.cellSize_Sm,
|
|
56
|
+
* terrainGrid: site.terrainGrid,
|
|
57
|
+
* obstacleGrid: site.obstacleGrid,
|
|
58
|
+
* elevationGrid: site.elevationGrid,
|
|
59
|
+
* slopeGrid: site.slopeGrid,
|
|
60
|
+
* };
|
|
61
|
+
* // Position entities at site.entryVectors[n].x_Sm / y_Sm before first stepWorld.
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export interface BattleTerrainParams {
|
|
65
|
+
/** Total battlefield width [SCALE.m]. */
|
|
66
|
+
width_Sm: number;
|
|
67
|
+
/** Total battlefield depth [SCALE.m]. */
|
|
68
|
+
height_Sm: number;
|
|
69
|
+
/** Cell size used for terrain grid lookups [SCALE.m]. */
|
|
70
|
+
cellSize_Sm: number;
|
|
71
|
+
/** Per-cell surface type — pass to `KernelContext.terrainGrid`. */
|
|
72
|
+
terrainGrid: TerrainGrid;
|
|
73
|
+
/** Per-cell cover fraction — pass to `KernelContext.obstacleGrid`. */
|
|
74
|
+
obstacleGrid: ObstacleGrid;
|
|
75
|
+
/** Per-cell elevation above ground [SCALE.m] — pass to `KernelContext.elevationGrid`. */
|
|
76
|
+
elevationGrid: ElevationGrid;
|
|
77
|
+
/** Per-cell slope direction and grade — pass to `KernelContext.slopeGrid`. */
|
|
78
|
+
slopeGrid: SlopeGrid;
|
|
79
|
+
/** Structural cover segments to place in the world before battle begins. */
|
|
80
|
+
coverSegments: CoverSegment[];
|
|
81
|
+
/** Entry spawn positions for each team. */
|
|
82
|
+
entryVectors: EntryVector[];
|
|
83
|
+
/**
|
|
84
|
+
* Dominant surface type — use `SURFACE_TRACTION[dominantSurface]` as
|
|
85
|
+
* the default `KernelContext.tractionCoeff`.
|
|
86
|
+
*/
|
|
87
|
+
dominantSurface: SurfaceType;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Context provided to `generateBattleSite`.
|
|
91
|
+
*/
|
|
92
|
+
export interface BattleSiteContext {
|
|
93
|
+
/** Campaign hex tile where the battle occurs. */
|
|
94
|
+
hexType: CampaignHexType;
|
|
95
|
+
/** Attacking team ids — they enter from the south (y ≈ 0). */
|
|
96
|
+
attackerTeamIds: number[];
|
|
97
|
+
/** Defending team ids — they enter from the north (y ≈ FIELD_HEIGHT). */
|
|
98
|
+
defenderTeamIds: number[];
|
|
99
|
+
/**
|
|
100
|
+
* World seed from the campaign — reserved for future micro-variance.
|
|
101
|
+
* Currently unused but forwarded for determinism documentation purposes.
|
|
102
|
+
*/
|
|
103
|
+
seed?: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Outcome produced by the tactical simulation, passed to `mergeBattleOutcome`.
|
|
107
|
+
*/
|
|
108
|
+
export interface BattleOutcome {
|
|
109
|
+
/** Final `WorldState` after the tactical battle ends. */
|
|
110
|
+
worldState: WorldState;
|
|
111
|
+
/**
|
|
112
|
+
* Battle duration in simulated seconds, added to `CampaignState.worldTime_s`.
|
|
113
|
+
*/
|
|
114
|
+
elapsedSeconds: number;
|
|
115
|
+
/**
|
|
116
|
+
* Entity ids of combatants incapacitated or captured on the losing side.
|
|
117
|
+
* Their weapons and armour will be transferred to the winning team's inventory.
|
|
118
|
+
*/
|
|
119
|
+
capturedEntityIds?: number[];
|
|
120
|
+
/**
|
|
121
|
+
* Team id of the victorious side. When `undefined` the battle is a draw and
|
|
122
|
+
* no equipment transfer occurs.
|
|
123
|
+
*/
|
|
124
|
+
winnerTeamId?: number;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build terrain, obstacle, elevation, and slope grids for a campaign hex type.
|
|
128
|
+
*
|
|
129
|
+
* Fully deterministic — produces the same output for the same `hexType` every
|
|
130
|
+
* call. Does not include `entryVectors`; use `generateBattleSite` for a full
|
|
131
|
+
* site including team spawn positions.
|
|
132
|
+
*
|
|
133
|
+
* Grid layout: 10 columns × 8 rows, each cell 10 m (100 000 SCALE.m).
|
|
134
|
+
* Total field: 100 m wide × 80 m deep.
|
|
135
|
+
*/
|
|
136
|
+
export declare function extractTerrainParams(hexType: CampaignHexType): Omit<BattleTerrainParams, "entryVectors">;
|
|
137
|
+
/**
|
|
138
|
+
* Generate a complete battle site for a campaign encounter.
|
|
139
|
+
*
|
|
140
|
+
* Calls `extractTerrainParams` and appends `EntryVector` entries for each
|
|
141
|
+
* attacking and defending team.
|
|
142
|
+
*
|
|
143
|
+
* Teams with more than three members reuse spawn positions (cyclic).
|
|
144
|
+
*/
|
|
145
|
+
export declare function generateBattleSite(ctx: BattleSiteContext): BattleTerrainParams;
|
|
146
|
+
/**
|
|
147
|
+
* Merge a completed tactical battle back into campaign state.
|
|
148
|
+
*
|
|
149
|
+
* **What this does:**
|
|
150
|
+
* - Advances `campaign.worldTime_s` by `outcome.elapsedSeconds`.
|
|
151
|
+
* - Removes entities that died in battle (`injury.dead === true`) from the
|
|
152
|
+
* campaign entity registry, location map, and inventory map.
|
|
153
|
+
* - Copies post-battle `injury` and `condition` state onto surviving campaign
|
|
154
|
+
* entities so wounds persist between encounters.
|
|
155
|
+
* - Transfers weapons and armour from `capturedEntityIds` to the winning
|
|
156
|
+
* team's first surviving entity inventory (item id → count).
|
|
157
|
+
* - Appends a human-readable battle summary to `campaign.log`.
|
|
158
|
+
*
|
|
159
|
+
* Mutates `campaign` in-place.
|
|
160
|
+
*/
|
|
161
|
+
export declare function mergeBattleOutcome(campaign: CampaignState, outcome: BattleOutcome): void;
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// src/terrain-bridge.ts — PA-5: Campaign ↔ Tactical Terrain Bridge
|
|
2
|
+
//
|
|
3
|
+
// Maps campaign hex tiles to tactical battlefield parameters consumable by
|
|
4
|
+
// KernelContext, and merges tactical battle results back into CampaignState.
|
|
5
|
+
//
|
|
6
|
+
// Typical workflow:
|
|
7
|
+
// 1. Army enters a forest hex on the campaign map.
|
|
8
|
+
// 2. Call generateBattleSite({ hexType: "forest", ... }) → BattleTerrainParams.
|
|
9
|
+
// 3. Build KernelContext from terrain params; run stepWorld until battle ends.
|
|
10
|
+
// 4. Call mergeBattleOutcome(campaign, { worldState, elapsedSeconds }) to
|
|
11
|
+
// apply casualties, injuries, and looted equipment back to campaign.
|
|
12
|
+
import { q } from "./units.js";
|
|
13
|
+
import { buildTerrainGrid, buildObstacleGrid, buildElevationGrid, buildSlopeGrid, terrainKey, } from "./sim/terrain.js";
|
|
14
|
+
import { createCoverSegment } from "./sim/cover.js";
|
|
15
|
+
// ── Field constants ────────────────────────────────────────────────────────────
|
|
16
|
+
/** Battlefield width [SCALE.m]. 100 m. */
|
|
17
|
+
export const FIELD_WIDTH_Sm = 1_000_000;
|
|
18
|
+
/** Battlefield depth [SCALE.m]. 80 m. */
|
|
19
|
+
export const FIELD_HEIGHT_Sm = 800_000;
|
|
20
|
+
/** Terrain cell size [SCALE.m]. 10 m per cell → 10 × 8 grid. */
|
|
21
|
+
export const CELL_SIZE_Sm = 100_000;
|
|
22
|
+
/** Number of grid columns (field width / cell size). */
|
|
23
|
+
export const GRID_COLS = 10;
|
|
24
|
+
/** Number of grid rows (field height / cell size). */
|
|
25
|
+
export const GRID_ROWS = 8;
|
|
26
|
+
/** Attacker spawn y [SCALE.m] — 5 m from the south (y=0) edge. */
|
|
27
|
+
export const ATTACKER_SPAWN_Y_Sm = 50_000;
|
|
28
|
+
/** Defender spawn y [SCALE.m] — 5 m from the north edge. */
|
|
29
|
+
export const DEFENDER_SPAWN_Y_Sm = (FIELD_HEIGHT_Sm - 50_000);
|
|
30
|
+
// ── extractTerrainParams ──────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Build terrain, obstacle, elevation, and slope grids for a campaign hex type.
|
|
33
|
+
*
|
|
34
|
+
* Fully deterministic — produces the same output for the same `hexType` every
|
|
35
|
+
* call. Does not include `entryVectors`; use `generateBattleSite` for a full
|
|
36
|
+
* site including team spawn positions.
|
|
37
|
+
*
|
|
38
|
+
* Grid layout: 10 columns × 8 rows, each cell 10 m (100 000 SCALE.m).
|
|
39
|
+
* Total field: 100 m wide × 80 m deep.
|
|
40
|
+
*/
|
|
41
|
+
export function extractTerrainParams(hexType) {
|
|
42
|
+
switch (hexType) {
|
|
43
|
+
case "plains": return _plains();
|
|
44
|
+
case "forest": return _forest();
|
|
45
|
+
case "hills": return _hills();
|
|
46
|
+
case "marsh": return _marsh();
|
|
47
|
+
case "urban": return _urban();
|
|
48
|
+
case "mountain": return _mountain();
|
|
49
|
+
case "river_crossing": return _riverCrossing();
|
|
50
|
+
case "coastal": return _coastal();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ── generateBattleSite ────────────────────────────────────────────────────────
|
|
54
|
+
/** Three evenly-spaced x spawn positions (15 m, 50 m, 85 m). */
|
|
55
|
+
const SPAWN_X_Sm = [150_000, 500_000, 850_000];
|
|
56
|
+
/**
|
|
57
|
+
* Generate a complete battle site for a campaign encounter.
|
|
58
|
+
*
|
|
59
|
+
* Calls `extractTerrainParams` and appends `EntryVector` entries for each
|
|
60
|
+
* attacking and defending team.
|
|
61
|
+
*
|
|
62
|
+
* Teams with more than three members reuse spawn positions (cyclic).
|
|
63
|
+
*/
|
|
64
|
+
export function generateBattleSite(ctx) {
|
|
65
|
+
const base = extractTerrainParams(ctx.hexType);
|
|
66
|
+
const entryVectors = [];
|
|
67
|
+
for (let i = 0; i < ctx.attackerTeamIds.length; i++) {
|
|
68
|
+
entryVectors.push({
|
|
69
|
+
teamId: ctx.attackerTeamIds[i],
|
|
70
|
+
x_Sm: SPAWN_X_Sm[i % SPAWN_X_Sm.length],
|
|
71
|
+
y_Sm: ATTACKER_SPAWN_Y_Sm,
|
|
72
|
+
facingY: 1,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
for (let i = 0; i < ctx.defenderTeamIds.length; i++) {
|
|
76
|
+
entryVectors.push({
|
|
77
|
+
teamId: ctx.defenderTeamIds[i],
|
|
78
|
+
x_Sm: SPAWN_X_Sm[i % SPAWN_X_Sm.length],
|
|
79
|
+
y_Sm: DEFENDER_SPAWN_Y_Sm,
|
|
80
|
+
facingY: -1,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return { ...base, entryVectors };
|
|
84
|
+
}
|
|
85
|
+
// ── mergeBattleOutcome ────────────────────────────────────────────────────────
|
|
86
|
+
/**
|
|
87
|
+
* Merge a completed tactical battle back into campaign state.
|
|
88
|
+
*
|
|
89
|
+
* **What this does:**
|
|
90
|
+
* - Advances `campaign.worldTime_s` by `outcome.elapsedSeconds`.
|
|
91
|
+
* - Removes entities that died in battle (`injury.dead === true`) from the
|
|
92
|
+
* campaign entity registry, location map, and inventory map.
|
|
93
|
+
* - Copies post-battle `injury` and `condition` state onto surviving campaign
|
|
94
|
+
* entities so wounds persist between encounters.
|
|
95
|
+
* - Transfers weapons and armour from `capturedEntityIds` to the winning
|
|
96
|
+
* team's first surviving entity inventory (item id → count).
|
|
97
|
+
* - Appends a human-readable battle summary to `campaign.log`.
|
|
98
|
+
*
|
|
99
|
+
* Mutates `campaign` in-place.
|
|
100
|
+
*/
|
|
101
|
+
export function mergeBattleOutcome(campaign, outcome) {
|
|
102
|
+
campaign.worldTime_s += outcome.elapsedSeconds;
|
|
103
|
+
const { worldState, capturedEntityIds = [], winnerTeamId } = outcome;
|
|
104
|
+
let killed = 0;
|
|
105
|
+
let survived = 0;
|
|
106
|
+
for (const entity of worldState.entities) {
|
|
107
|
+
const campaignEntity = campaign.entities.get(entity.id);
|
|
108
|
+
if (campaignEntity === undefined)
|
|
109
|
+
continue; // not in this campaign
|
|
110
|
+
if (_isDead(entity)) {
|
|
111
|
+
campaign.entities.delete(entity.id);
|
|
112
|
+
campaign.entityLocations.delete(entity.id);
|
|
113
|
+
campaign.entityInventories.delete(entity.id);
|
|
114
|
+
killed++;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Carry forward post-battle wounds and psychological state
|
|
118
|
+
if (entity.injury !== undefined)
|
|
119
|
+
campaignEntity.injury = entity.injury;
|
|
120
|
+
if (entity.condition !== undefined)
|
|
121
|
+
campaignEntity.condition = entity.condition;
|
|
122
|
+
survived++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Equipment transfer: looted gear from captured/incapacitated enemies
|
|
126
|
+
if (winnerTeamId !== undefined && capturedEntityIds.length > 0) {
|
|
127
|
+
const winnerId = _firstAliveInTeam(worldState, winnerTeamId);
|
|
128
|
+
if (winnerId !== undefined) {
|
|
129
|
+
const winInv = campaign.entityInventories.get(winnerId) ?? new Map();
|
|
130
|
+
for (const capturedId of capturedEntityIds) {
|
|
131
|
+
// Carry items from campaign inventory
|
|
132
|
+
const inv = campaign.entityInventories.get(capturedId);
|
|
133
|
+
if (inv !== undefined) {
|
|
134
|
+
for (const [itemId, count] of inv) {
|
|
135
|
+
winInv.set(itemId, (winInv.get(itemId) ?? 0) + count);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Transfer equipped items (weapons, armour) by id
|
|
139
|
+
const capEntity = campaign.entities.get(capturedId);
|
|
140
|
+
if (capEntity !== undefined) {
|
|
141
|
+
for (const item of capEntity.loadout.items) {
|
|
142
|
+
winInv.set(item.id, (winInv.get(item.id) ?? 0) + 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
campaign.entityInventories.set(winnerId, winInv);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const outcome_note = winnerTeamId !== undefined
|
|
150
|
+
? ` Team ${winnerTeamId} victorious.`
|
|
151
|
+
: " Draw.";
|
|
152
|
+
campaign.log.push({
|
|
153
|
+
worldTime_s: campaign.worldTime_s,
|
|
154
|
+
text: `Battle concluded (${survived} survived, ${killed} killed).${outcome_note}`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// ── Internal helpers ──────────────────────────────────────────────────────────
|
|
158
|
+
function _isDead(entity) {
|
|
159
|
+
return entity.injury?.dead === true;
|
|
160
|
+
}
|
|
161
|
+
function _firstAliveInTeam(world, teamId) {
|
|
162
|
+
for (const e of world.entities) {
|
|
163
|
+
if (e.teamId === teamId && !_isDead(e))
|
|
164
|
+
return e.id;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
function _base() {
|
|
169
|
+
return {
|
|
170
|
+
width_Sm: FIELD_WIDTH_Sm,
|
|
171
|
+
height_Sm: FIELD_HEIGHT_Sm,
|
|
172
|
+
cellSize_Sm: CELL_SIZE_Sm,
|
|
173
|
+
terrainGrid: new Map(),
|
|
174
|
+
obstacleGrid: new Map(),
|
|
175
|
+
elevationGrid: new Map(),
|
|
176
|
+
slopeGrid: new Map(),
|
|
177
|
+
coverSegments: [],
|
|
178
|
+
dominantSurface: "normal",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function ck(col, row) {
|
|
182
|
+
return terrainKey(col, row);
|
|
183
|
+
}
|
|
184
|
+
/** Plains: open ground, two dirt berm defensive lines. */
|
|
185
|
+
function _plains() {
|
|
186
|
+
const r = _base();
|
|
187
|
+
r.coverSegments.push(createCoverSegment("plains_berm_s", 200_000, 250_000, 600_000, 8_000, "dirt"), createCoverSegment("plains_berm_n", 200_000, 550_000, 600_000, 8_000, "dirt"));
|
|
188
|
+
return r;
|
|
189
|
+
}
|
|
190
|
+
/** Forest: muddy undergrowth, partial cover, two wood tree-lines flanking a central path. */
|
|
191
|
+
function _forest() {
|
|
192
|
+
const r = _base();
|
|
193
|
+
r.dominantSurface = "mud";
|
|
194
|
+
const terrain = {};
|
|
195
|
+
const obstacles = {};
|
|
196
|
+
for (let col = 0; col < GRID_COLS; col++) {
|
|
197
|
+
for (let row = 0; row < GRID_ROWS; row++) {
|
|
198
|
+
const key = ck(col, row);
|
|
199
|
+
if (row !== 3 && row !== 4) { // rows 3-4 = natural path (no mud)
|
|
200
|
+
terrain[key] = "mud";
|
|
201
|
+
obstacles[key] = q(0.40); // dense undergrowth — 40% cover
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
r.terrainGrid = buildTerrainGrid(terrain);
|
|
206
|
+
r.obstacleGrid = buildObstacleGrid(obstacles);
|
|
207
|
+
// Tree-lines flanking the path (y = 25 m and y = 50 m)
|
|
208
|
+
r.coverSegments.push(createCoverSegment("forest_tree_s", 0, 250_000, 1_000_000, 12_000, "wood"), createCoverSegment("forest_tree_n", 0, 500_000, 1_000_000, 12_000, "wood"));
|
|
209
|
+
return r;
|
|
210
|
+
}
|
|
211
|
+
/** Hills: gradient elevation south-to-north, stone cover on the crest. */
|
|
212
|
+
function _hills() {
|
|
213
|
+
const r = _base();
|
|
214
|
+
r.dominantSurface = "slope_up";
|
|
215
|
+
const terrain = {};
|
|
216
|
+
const elevation = {};
|
|
217
|
+
const slopes = {};
|
|
218
|
+
for (let col = 0; col < GRID_COLS; col++) {
|
|
219
|
+
for (let row = 0; row < GRID_ROWS; row++) {
|
|
220
|
+
const key = ck(col, row);
|
|
221
|
+
if (row < 4) {
|
|
222
|
+
// South half — uphill approach; elevation rises 5 m per row (0–15 m)
|
|
223
|
+
terrain[key] = "slope_up";
|
|
224
|
+
elevation[key] = (row * 50_000);
|
|
225
|
+
slopes[key] = { type: "uphill", grade: q(0.50) };
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// North half — downhill on far side of crest
|
|
229
|
+
terrain[key] = "slope_down";
|
|
230
|
+
elevation[key] = ((GRID_ROWS - 1 - row) * 50_000);
|
|
231
|
+
slopes[key] = { type: "downhill", grade: q(0.50) };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
r.terrainGrid = buildTerrainGrid(terrain);
|
|
236
|
+
r.elevationGrid = buildElevationGrid(elevation);
|
|
237
|
+
r.slopeGrid = buildSlopeGrid(slopes);
|
|
238
|
+
// Stone wall along the ridgeline (y ≈ 40 m)
|
|
239
|
+
r.coverSegments.push(createCoverSegment("hills_ridgeline", 100_000, 400_000, 800_000, 10_000, "stone"));
|
|
240
|
+
return r;
|
|
241
|
+
}
|
|
242
|
+
/** Marsh: all-mud terrain, no cover — speed heavily penalised. */
|
|
243
|
+
function _marsh() {
|
|
244
|
+
const r = _base();
|
|
245
|
+
r.dominantSurface = "mud";
|
|
246
|
+
const terrain = {};
|
|
247
|
+
for (let col = 0; col < GRID_COLS; col++) {
|
|
248
|
+
for (let row = 0; row < GRID_ROWS; row++) {
|
|
249
|
+
terrain[ck(col, row)] = "mud";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
r.terrainGrid = buildTerrainGrid(terrain);
|
|
253
|
+
return r;
|
|
254
|
+
}
|
|
255
|
+
/** Urban: dense stone and wood cover forming a street grid; partial obstacle cells. */
|
|
256
|
+
function _urban() {
|
|
257
|
+
const r = _base();
|
|
258
|
+
// Stone building walls — south block, mid block, north block
|
|
259
|
+
r.coverSegments.push(createCoverSegment("urban_s1", 0, 200_000, 350_000, 20_000, "stone"), createCoverSegment("urban_s2", 450_000, 200_000, 350_000, 20_000, "stone"), createCoverSegment("urban_s3", 200_000, 300_000, 200_000, 20_000, "stone"), createCoverSegment("urban_m1", 0, 400_000, 250_000, 20_000, "stone"), createCoverSegment("urban_m2", 350_000, 400_000, 300_000, 20_000, "stone"), createCoverSegment("urban_m3", 750_000, 400_000, 250_000, 20_000, "stone"), createCoverSegment("urban_n1", 100_000, 600_000, 350_000, 20_000, "stone"), createCoverSegment("urban_n2", 550_000, 600_000, 350_000, 20_000, "stone"), createCoverSegment("urban_barr1", 350_000, 350_000, 100_000, 10_000, "wood"), createCoverSegment("urban_barr2", 350_000, 500_000, 100_000, 10_000, "wood"));
|
|
260
|
+
// Partial obstacles in building interiors (~50% cover)
|
|
261
|
+
r.obstacleGrid = buildObstacleGrid({
|
|
262
|
+
[ck(1, 2)]: q(0.50),
|
|
263
|
+
[ck(5, 2)]: q(0.50),
|
|
264
|
+
[ck(2, 4)]: q(0.50),
|
|
265
|
+
[ck(7, 4)]: q(0.50),
|
|
266
|
+
});
|
|
267
|
+
return r;
|
|
268
|
+
}
|
|
269
|
+
/** Mountain: steep icy ascent, high elevation, rocky outcrops. */
|
|
270
|
+
function _mountain() {
|
|
271
|
+
const r = _base();
|
|
272
|
+
r.dominantSurface = "slope_up";
|
|
273
|
+
const terrain = {};
|
|
274
|
+
const elevation = {};
|
|
275
|
+
const slopes = {};
|
|
276
|
+
for (let col = 0; col < GRID_COLS; col++) {
|
|
277
|
+
for (let row = 0; row < GRID_ROWS; row++) {
|
|
278
|
+
const key = ck(col, row);
|
|
279
|
+
elevation[key] = (row * 100_000); // 0–70 m rise
|
|
280
|
+
terrain[key] = row >= 4 ? "ice" : "slope_up";
|
|
281
|
+
slopes[key] = {
|
|
282
|
+
type: "uphill",
|
|
283
|
+
grade: (row >= 4 ? q(0.80) : q(0.60)),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
r.terrainGrid = buildTerrainGrid(terrain);
|
|
288
|
+
r.elevationGrid = buildElevationGrid(elevation);
|
|
289
|
+
r.slopeGrid = buildSlopeGrid(slopes);
|
|
290
|
+
// Rocky outcrops for cover
|
|
291
|
+
r.coverSegments.push(createCoverSegment("mtn_rock1", 100_000, 200_000, 150_000, 15_000, "stone"), createCoverSegment("mtn_rock2", 600_000, 350_000, 200_000, 15_000, "stone"), createCoverSegment("mtn_rock3", 300_000, 500_000, 150_000, 15_000, "stone"));
|
|
292
|
+
return r;
|
|
293
|
+
}
|
|
294
|
+
/** River crossing: muddy ford in the center, sandbag cover on the defending bank. */
|
|
295
|
+
function _riverCrossing() {
|
|
296
|
+
const r = _base();
|
|
297
|
+
r.dominantSurface = "mud";
|
|
298
|
+
// Mud band at rows 3-4 (30–50 m) — the ford
|
|
299
|
+
const terrain = {};
|
|
300
|
+
for (let col = 0; col < GRID_COLS; col++) {
|
|
301
|
+
terrain[ck(col, 3)] = "mud";
|
|
302
|
+
terrain[ck(col, 4)] = "mud";
|
|
303
|
+
}
|
|
304
|
+
r.terrainGrid = buildTerrainGrid(terrain);
|
|
305
|
+
// Sandbag defensive line on the far (north) bank
|
|
306
|
+
r.coverSegments.push(createCoverSegment("river_cover_w", 50_000, 500_000, 350_000, 12_000, "sandbag"), createCoverSegment("river_cover_e", 600_000, 500_000, 350_000, 12_000, "sandbag"));
|
|
307
|
+
return r;
|
|
308
|
+
}
|
|
309
|
+
/** Coastal: muddy beach approach (south 2 rows), dunes and rocky outcrops for cover. */
|
|
310
|
+
function _coastal() {
|
|
311
|
+
const r = _base();
|
|
312
|
+
// Soft sand / surf zone — rows 0-1 (0–20 m from south)
|
|
313
|
+
const terrain = {};
|
|
314
|
+
for (let col = 0; col < GRID_COLS; col++) {
|
|
315
|
+
terrain[ck(col, 0)] = "mud";
|
|
316
|
+
terrain[ck(col, 1)] = "mud";
|
|
317
|
+
}
|
|
318
|
+
r.terrainGrid = buildTerrainGrid(terrain);
|
|
319
|
+
// Dunes and rocky coastal outcrops
|
|
320
|
+
r.coverSegments.push(createCoverSegment("coastal_dune1", 100_000, 150_000, 300_000, 8_000, "dirt"), createCoverSegment("coastal_dune2", 600_000, 150_000, 300_000, 8_000, "dirt"), createCoverSegment("coastal_rock1", 0, 350_000, 200_000, 12_000, "stone"), createCoverSegment("coastal_rock2", 800_000, 450_000, 200_000, 12_000, "stone"));
|
|
321
|
+
return r;
|
|
322
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@its-not-rocket-science/ananke",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.54",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -181,6 +181,10 @@
|
|
|
181
181
|
"./content-pack": {
|
|
182
182
|
"import": "./dist/src/content-pack.js",
|
|
183
183
|
"types": "./dist/src/content-pack.d.ts"
|
|
184
|
+
},
|
|
185
|
+
"./terrain-bridge": {
|
|
186
|
+
"import": "./dist/src/terrain-bridge.js",
|
|
187
|
+
"types": "./dist/src/terrain-bridge.d.ts"
|
|
184
188
|
}
|
|
185
189
|
},
|
|
186
190
|
"workspaces": [
|