@dreadwood-types/types 0.1.49 → 0.1.51
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/dist/adventure.d.ts +6 -6
- package/dist/adventure.d.ts.map +1 -1
- package/dist/appearance.d.ts +16 -6
- package/dist/appearance.d.ts.map +1 -1
- package/dist/appearance.js +46 -34
- package/dist/appearance.js.map +1 -1
- package/dist/bank.d.ts.map +1 -1
- package/dist/bearFloes.d.ts +83 -0
- package/dist/bearFloes.d.ts.map +1 -0
- package/dist/bearFloes.js +126 -0
- package/dist/bearFloes.js.map +1 -0
- package/dist/blacksmithing.d.ts.map +1 -1
- package/dist/blacksmithing.js +78 -1
- package/dist/blacksmithing.js.map +1 -1
- package/dist/bloodRiver.d.ts +9 -6
- package/dist/bloodRiver.d.ts.map +1 -1
- package/dist/bloodRiver.js +19 -17
- package/dist/bloodRiver.js.map +1 -1
- package/dist/brokenRoad.d.ts.map +1 -1
- package/dist/capital.d.ts +541 -180
- package/dist/capital.d.ts.map +1 -1
- package/dist/capital.js +934 -184
- package/dist/capital.js.map +1 -1
- package/dist/charcoal.d.ts +43 -0
- package/dist/charcoal.d.ts.map +1 -0
- package/dist/charcoal.js +78 -0
- package/dist/charcoal.js.map +1 -0
- package/dist/clientConfig.d.ts +7 -2
- package/dist/clientConfig.d.ts.map +1 -1
- package/dist/clientConfig.js +13 -3
- package/dist/clientConfig.js.map +1 -1
- package/dist/constants.d.ts +169 -9
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +328 -17
- package/dist/constants.js.map +1 -1
- package/dist/dragonfire.d.ts +75 -0
- package/dist/dragonfire.d.ts.map +1 -0
- package/dist/dragonfire.js +73 -0
- package/dist/dragonfire.js.map +1 -0
- package/dist/dungeonCharts.d.ts +18 -0
- package/dist/dungeonCharts.d.ts.map +1 -0
- package/dist/dungeonCharts.js +67 -0
- package/dist/dungeonCharts.js.map +1 -0
- package/dist/equipment.d.ts +2 -0
- package/dist/equipment.d.ts.map +1 -1
- package/dist/equipment.js +33 -1
- package/dist/equipment.js.map +1 -1
- package/dist/frozenSet.d.ts +36 -0
- package/dist/frozenSet.d.ts.map +1 -0
- package/dist/frozenSet.js +58 -0
- package/dist/frozenSet.js.map +1 -0
- package/dist/goblinCave.d.ts +4 -4
- package/dist/goblinCave.d.ts.map +1 -1
- package/dist/goblinCave.js +1 -1
- package/dist/groundItems.d.ts.map +1 -1
- package/dist/groundItems.js +16 -0
- package/dist/groundItems.js.map +1 -1
- package/dist/harbour.d.ts +60 -24
- package/dist/harbour.d.ts.map +1 -1
- package/dist/harbour.js +84 -50
- package/dist/harbour.js.map +1 -1
- package/dist/iceDepths.d.ts +11 -0
- package/dist/iceDepths.d.ts.map +1 -1
- package/dist/iceDepths.js +14 -0
- package/dist/iceDepths.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interiorFurniture.d.ts +57 -0
- package/dist/interiorFurniture.d.ts.map +1 -0
- package/dist/interiorFurniture.js +159 -0
- package/dist/interiorFurniture.js.map +1 -0
- package/dist/landmarks.d.ts +77 -58
- package/dist/landmarks.d.ts.map +1 -1
- package/dist/landmarks.js +145 -119
- package/dist/landmarks.js.map +1 -1
- package/dist/messages.d.ts +74 -5
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js.map +1 -1
- package/dist/mineCave.d.ts +7 -7
- package/dist/mineCave.d.ts.map +1 -1
- package/dist/mining.d.ts +8 -0
- package/dist/mining.d.ts.map +1 -1
- package/dist/mining.js +8 -0
- package/dist/mining.js.map +1 -1
- package/dist/questGuide.d.ts +30 -0
- package/dist/questGuide.d.ts.map +1 -0
- package/dist/questGuide.js +3 -0
- package/dist/questGuide.js.map +1 -0
- package/dist/quests.d.ts +29 -0
- package/dist/quests.d.ts.map +1 -1
- package/dist/quests.js +70 -0
- package/dist/quests.js.map +1 -1
- package/dist/snow.d.ts +2 -2
- package/dist/snow.d.ts.map +1 -1
- package/dist/snow.js +8 -5
- package/dist/snow.js.map +1 -1
- package/dist/spiderCave.d.ts.map +1 -1
- package/dist/woodwork.d.ts +8 -2
- package/dist/woodwork.d.ts.map +1 -1
- package/dist/woodwork.js +13 -3
- package/dist/woodwork.js.map +1 -1
- package/dist/xp.d.ts +18 -7
- package/dist/xp.d.ts.map +1 -1
- package/dist/xp.js +20 -9
- package/dist/xp.js.map +1 -1
- package/dist/zones.d.ts +5 -0
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +8 -3
- package/dist/zones.js.map +1 -1
- package/package.json +1 -1
package/dist/constants.js
CHANGED
|
@@ -63,14 +63,36 @@ export const PLAYABLE_CHUNKS_X = FARMLAND_CHUNKS_X + VILLAGE_CHUNKS_X + EASTLAND
|
|
|
63
63
|
export const PLAYABLE_CHUNKS_Z = 3;
|
|
64
64
|
export const FARMLAND_WIDTH = CHUNK_SIZE * FARMLAND_CHUNKS_X;
|
|
65
65
|
export const WORLD_EAST_CHUNKS = 2;
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The northern extension (James, 2026-09-25: "extend the map north a bit"
|
|
68
|
+
* for the Frozen Shore and its polar bears): two chunks of new rows above the
|
|
69
|
+
* old north edge. Every surface place moved south by NORTH_SHIFT rows with
|
|
70
|
+
* it, pad-relative ones through WORLD_NORTH_PAD and the hand-written world z
|
|
71
|
+
* values by +NORTH_SHIFT in place; the underground strip (x >=
|
|
72
|
+
* UNDERGROUND_ORIGIN_X) did not move. Coordinate hashes read `legacyZ` so the
|
|
73
|
+
* scatter stayed where it was, and saved characters are moved once, at the
|
|
74
|
+
* backend's boot (db/characters.ts, WORLD_REVISION). Proved with the
|
|
75
|
+
* backend's scripts/world-fingerprint.ts and world-shift-diff.ts.
|
|
76
|
+
*/
|
|
77
|
+
export const WORLD_NORTH_EXTENSION_CHUNKS = 2;
|
|
78
|
+
export const NORTH_SHIFT = CHUNK_SIZE * WORLD_NORTH_EXTENSION_CHUNKS;
|
|
79
|
+
export const WORLD_NORTH_CHUNKS = 23 + WORLD_NORTH_EXTENSION_CHUNKS;
|
|
80
|
+
/**
|
|
81
|
+
* The world's layout revision, saved on each character: 1 is the 64-row
|
|
82
|
+
* northern extension. A character saved before it (no revision) is moved
|
|
83
|
+
* NORTH_SHIFT rows south once, when the backend boots, if it was on the
|
|
84
|
+
* surface.
|
|
85
|
+
*/
|
|
86
|
+
export const WORLD_REVISION = 1;
|
|
67
87
|
/**
|
|
68
88
|
* Open sea south of the playable strip. Padding the far side of the beach,
|
|
69
89
|
* not the near side, so every existing world coordinate is untouched - only
|
|
70
90
|
* WORLD_HEIGHT grows. It exists so the Long Pier has somewhere to go: the
|
|
71
91
|
* playable ocean is fourteen rows deep, which is a jetty, not a pier.
|
|
92
|
+
* Five since 2026-09-24, when the pier stretched to 130 rows: the ship loop
|
|
93
|
+
* (SeaTraffic) needs its offshore leg past the pier head's clearance.
|
|
72
94
|
*/
|
|
73
|
-
export const WORLD_SOUTH_CHUNKS =
|
|
95
|
+
export const WORLD_SOUTH_CHUNKS = 5;
|
|
74
96
|
export const DEMO_CHUNKS_X = PLAYABLE_CHUNKS_X + WORLD_EAST_CHUNKS;
|
|
75
97
|
export const DEMO_CHUNKS_Z = PLAYABLE_CHUNKS_Z + WORLD_NORTH_CHUNKS + WORLD_SOUTH_CHUNKS;
|
|
76
98
|
export const PLAYABLE_WIDTH = CHUNK_SIZE * PLAYABLE_CHUNKS_X;
|
|
@@ -93,6 +115,14 @@ export const GRID_CHUNKS_X = DEMO_CHUNKS_X + UNDERGROUND_CHUNKS_X;
|
|
|
93
115
|
export const GRID_CHUNKS_Z = DEMO_CHUNKS_Z;
|
|
94
116
|
export const GRID_WIDTH = CHUNK_SIZE * GRID_CHUNKS_X;
|
|
95
117
|
export const GRID_HEIGHT = CHUNK_SIZE * GRID_CHUNKS_Z;
|
|
118
|
+
/**
|
|
119
|
+
* The z a coordinate hash should read: the tile's z from before the northern
|
|
120
|
+
* extension, so scatter, noise and per-tile variety landed where they always
|
|
121
|
+
* had. The underground strip never moved, so its z is its own.
|
|
122
|
+
*/
|
|
123
|
+
export function legacyZ(x, z) {
|
|
124
|
+
return x < UNDERGROUND_ORIGIN_X ? z - NORTH_SHIFT : z;
|
|
125
|
+
}
|
|
96
126
|
/**
|
|
97
127
|
* Kinds that never carry an Identify marker, because nothing happens when you
|
|
98
128
|
* click them.
|
|
@@ -149,8 +179,11 @@ export const NON_IDENTIFIABLE_KINDS = new Set([
|
|
|
149
179
|
'stalagmite',
|
|
150
180
|
'icecrystal',
|
|
151
181
|
'iceshard',
|
|
182
|
+
'icefloe',
|
|
152
183
|
'dreadprop',
|
|
153
184
|
'depthsprop',
|
|
185
|
+
'frozenchest',
|
|
186
|
+
'charcoalmound',
|
|
154
187
|
'icefall',
|
|
155
188
|
'icearch',
|
|
156
189
|
'icecell',
|
|
@@ -363,6 +396,10 @@ export function entityBlocksWalk(kind) {
|
|
|
363
396
|
// The Depths' dressing is solid unless the style says floor (grates,
|
|
364
397
|
// sigils, spills, lanterns hung from the dark); see visitBlockedWalkOffsets.
|
|
365
398
|
kind === 'depthsprop' ||
|
|
399
|
+
// The Frozen Chest is an iron-bound box you open, not walk over.
|
|
400
|
+
kind === 'frozenchest' ||
|
|
401
|
+
// A charcoal mound is a heap of earth over smouldering logs.
|
|
402
|
+
kind === 'charcoalmound' ||
|
|
366
403
|
// A table of fish and knives is furniture, not floor.
|
|
367
404
|
kind === 'baittable' ||
|
|
368
405
|
// The keep is a building you cannot enter yet; a plinth and a basin are solid.
|
|
@@ -382,6 +419,8 @@ export function entityBlocksWalk(kind) {
|
|
|
382
419
|
kind === 'gardenwall' ||
|
|
383
420
|
kind === 'banner' ||
|
|
384
421
|
kind === 'yardprop' ||
|
|
422
|
+
// The Blender buildings: a hollow ring or solid, by model (CAPITAL_BUILDING_MODELS).
|
|
423
|
+
kind === 'capitalbuilding' ||
|
|
385
424
|
// Crystals and pillars are solid; a gate bars its whole span and a rune
|
|
386
425
|
// arch its two piers (see visitBlockedWalkOffsets). Frost traps and rope
|
|
387
426
|
// bridges are floor you walk over.
|
|
@@ -704,6 +743,9 @@ export function buildingFloorSurface(kind, style) {
|
|
|
704
743
|
return SurfaceType.Boards;
|
|
705
744
|
if (kind === 'temple' || kind === 'forgehall' || kind === 'manor' || kind === 'castle')
|
|
706
745
|
return SurfaceType.Flagstones;
|
|
746
|
+
// The Blender halls (the Grand Exchange, the Main Forge) stand on flags.
|
|
747
|
+
if (kind === 'capitalbuilding')
|
|
748
|
+
return SurfaceType.Flagstones;
|
|
707
749
|
return SurfaceType.Path;
|
|
708
750
|
}
|
|
709
751
|
/**
|
|
@@ -777,6 +819,68 @@ export function rowhouseTokens(style) {
|
|
|
777
819
|
const parts = (style ?? '').split('-');
|
|
778
820
|
return new Set(parts.slice(1, Math.max(1, parts.length - 1)));
|
|
779
821
|
}
|
|
822
|
+
export const CAPITAL_BUILDING_MODELS = {
|
|
823
|
+
// v003 (capital-workshop/scripts/grandexchange_v003.py): 19 x 13, door S.
|
|
824
|
+
// Porch columns at x +-2.15 and the small planters at x +-4.7 stand one
|
|
825
|
+
// row out (dz 7); the tree planters at x +-9.8 too; the lamp posts at x
|
|
826
|
+
// +-3.4 and the notice boards at x +-6.6 two rows out (dz 8).
|
|
827
|
+
grandexchange: {
|
|
828
|
+
across: 19,
|
|
829
|
+
deep: 13,
|
|
830
|
+
mouth: 1,
|
|
831
|
+
frontProps: [[-2, 7], [2, 7], [-5, 7], [5, 7], [-10, 7], [10, 7], [-3, 8], [3, 8], [-7, 8], [7, 8]],
|
|
832
|
+
},
|
|
833
|
+
mainforge: { across: 17, deep: 13, mouth: 5 },
|
|
834
|
+
smelter: { across: 7, deep: 7, mouth: 0, solid: true },
|
|
835
|
+
// The King's Statue v003 is 5 across (its cypresses and flower beds flank the
|
|
836
|
+
// plinth, as the Noble sheet draws it) and 3 deep; its plan rows reserve 5x5.
|
|
837
|
+
kingstatue: { across: 5, deep: 3, mouth: 0, solid: true },
|
|
838
|
+
keeplion: { across: 3, deep: 3, mouth: 0, solid: true },
|
|
839
|
+
victorystatue: { across: 5, deep: 5, mouth: 0, solid: true },
|
|
840
|
+
};
|
|
841
|
+
/** The model id in a `capitalbuilding` style (its first dash token), when it names one. */
|
|
842
|
+
export function capitalBuildingModelId(style) {
|
|
843
|
+
const id = (style ?? '').split('-')[0] ?? '';
|
|
844
|
+
return Object.prototype.hasOwnProperty.call(CAPITAL_BUILDING_MODELS, id) ? id : null;
|
|
845
|
+
}
|
|
846
|
+
export function capitalBuildingModel(style) {
|
|
847
|
+
const id = capitalBuildingModelId(style);
|
|
848
|
+
return id ? CAPITAL_BUILDING_MODELS[id] : null;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* A local-frame offset (door on +Z) turned to the building's facing: the
|
|
852
|
+
* same turn the assets make (`facingYaw`) and visitFurnitureTiles applies.
|
|
853
|
+
*/
|
|
854
|
+
export function turnBuildingOffset(lx, lz, facing) {
|
|
855
|
+
if (facing === 'negZ')
|
|
856
|
+
return [-lx, -lz];
|
|
857
|
+
if (facing === 'posX')
|
|
858
|
+
return [lz, -lx];
|
|
859
|
+
if (facing === 'negX')
|
|
860
|
+
return [-lz, lx];
|
|
861
|
+
return [lx, lz];
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* THE MOUTH RULE (2026-09-25). How many tiles either side of the door's
|
|
865
|
+
* middle tile are open on the door side: 2 for a `mouth5` style token, 1 for
|
|
866
|
+
* `mouth3`, 3 for `mouth7` (any odd `mouthN`), else the Blender model's own
|
|
867
|
+
* mouth, else 0 (a one-tile door). The collision (visitBlockedWalkOffsets,
|
|
868
|
+
* so the server and the client's prediction) and the roof hiding
|
|
869
|
+
* (isBuildingInteriorOffset) both take it, so the three always agree.
|
|
870
|
+
*/
|
|
871
|
+
export function buildingDoorHalfWidth(kind, style) {
|
|
872
|
+
for (const token of (style ?? '').split('-')) {
|
|
873
|
+
const m = /^mouth(\d+)$/.exec(token);
|
|
874
|
+
if (m)
|
|
875
|
+
return Math.max(0, Math.floor((Number(m[1]) - 1) / 2));
|
|
876
|
+
}
|
|
877
|
+
if (kind === 'capitalbuilding') {
|
|
878
|
+
const model = capitalBuildingModel(style);
|
|
879
|
+
if (model && model.mouth > 1)
|
|
880
|
+
return Math.floor((model.mouth - 1) / 2);
|
|
881
|
+
}
|
|
882
|
+
return 0;
|
|
883
|
+
}
|
|
780
884
|
/**
|
|
781
885
|
* citywall: the piece stands on the wall's outer row and is two tiles thick
|
|
782
886
|
* inward (n: +z is inward, s: -z, w: +x, e: -x). A bastion is a 3x3 drum on
|
|
@@ -822,19 +926,43 @@ export const GATEHOUSE_SIDE_HALF_SPAN = 4;
|
|
|
822
926
|
export const GATEHOUSE_HALF_DEPTH = 3;
|
|
823
927
|
export const GATEHOUSE_SIDE_HALF_DEPTH = 2;
|
|
824
928
|
export const GATEHOUSE_PASSAGE_HALF_W = 2;
|
|
929
|
+
/**
|
|
930
|
+
* A gatehouse's facing: the LAST n/s/e/w token of its style (`main-w`,
|
|
931
|
+
* `side-e`, `main-shut-n`). The facing is always written last, because the
|
|
932
|
+
* frontend turns the model by the style's last token.
|
|
933
|
+
*/
|
|
934
|
+
export function gatehouseFacing(style) {
|
|
935
|
+
const tokens = (style ?? 'main-w').split('-');
|
|
936
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
937
|
+
const t = tokens[i];
|
|
938
|
+
if (t === 'n' || t === 's' || t === 'e' || t === 'w')
|
|
939
|
+
return t;
|
|
940
|
+
}
|
|
941
|
+
return 'w';
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* A barred gatehouse (2026-09-25, the Capital's North and South Gates):
|
|
945
|
+
* a `shut` token (`main-shut-n`) lets the portcullis down, and the passage
|
|
946
|
+
* blocks like the towers, so the collision alone keeps it shut whatever the
|
|
947
|
+
* builder does with the wall rows under it.
|
|
948
|
+
*/
|
|
949
|
+
export function isGatehouseShut(style) {
|
|
950
|
+
return (style ?? '').split('-').includes('shut');
|
|
951
|
+
}
|
|
825
952
|
/** Which way the passage runs: -w/-e styles run along x, -n/-s along z. */
|
|
826
953
|
export function gatehousePassageAlongX(style) {
|
|
827
|
-
const facing = (style
|
|
954
|
+
const facing = gatehouseFacing(style);
|
|
828
955
|
return facing === 'w' || facing === 'e';
|
|
829
956
|
}
|
|
830
957
|
export function visitGatehouseOffsets(style, visit) {
|
|
831
958
|
const alongX = gatehousePassageAlongX(style);
|
|
832
959
|
const main = (style ?? 'main-w').startsWith('main');
|
|
960
|
+
const shut = isGatehouseShut(style);
|
|
833
961
|
const span = main ? GATEHOUSE_HALF_SPAN : GATEHOUSE_SIDE_HALF_SPAN;
|
|
834
962
|
const depth = main ? GATEHOUSE_HALF_DEPTH : GATEHOUSE_SIDE_HALF_DEPTH;
|
|
835
963
|
for (let along = -depth; along <= depth; along++) {
|
|
836
964
|
for (let across = -span; across <= span; across++) {
|
|
837
|
-
if (Math.abs(across) <= GATEHOUSE_PASSAGE_HALF_W)
|
|
965
|
+
if (!shut && Math.abs(across) <= GATEHOUSE_PASSAGE_HALF_W)
|
|
838
966
|
continue;
|
|
839
967
|
if (alongX)
|
|
840
968
|
visit(along, across);
|
|
@@ -849,6 +977,17 @@ export function buildingFootprintExtents(kind, scale, style) {
|
|
|
849
977
|
}
|
|
850
978
|
if (kind === 'castle')
|
|
851
979
|
return { radiusX: CASTLE_KEEP_HALF_X, radiusZ: CASTLE_KEEP_HALF_Z };
|
|
980
|
+
if (kind === 'capitalbuilding') {
|
|
981
|
+
const model = capitalBuildingModel(style);
|
|
982
|
+
if (!model)
|
|
983
|
+
return { radiusX: 0, radiusZ: 0 };
|
|
984
|
+
const across = Math.floor((model.across - 1) / 2);
|
|
985
|
+
const deep = Math.floor((model.deep - 1) / 2);
|
|
986
|
+
const facing = stoneHouseDoorFacing(style);
|
|
987
|
+
if (facing === 'posX' || facing === 'negX')
|
|
988
|
+
return { radiusX: deep, radiusZ: across };
|
|
989
|
+
return { radiusX: across, radiusZ: deep };
|
|
990
|
+
}
|
|
852
991
|
if (kind === 'stonehouse') {
|
|
853
992
|
const variant = stoneHouseVariant(style);
|
|
854
993
|
if (variant === 'lodge') {
|
|
@@ -902,19 +1041,26 @@ export function buildingFootprintRadius(kind, scale, style) {
|
|
|
902
1041
|
const { radiusX, radiusZ } = buildingFootprintExtents(kind, scale, style);
|
|
903
1042
|
return Math.max(radiusX, radiusZ);
|
|
904
1043
|
}
|
|
905
|
-
|
|
1044
|
+
/**
|
|
1045
|
+
* The door opening on a building's ring. `halfW` is the mouth rule's
|
|
1046
|
+
* half-width (buildingDoorHalfWidth): 0 is the one middle tile, 2 a
|
|
1047
|
+
* five-wide mouth. The corners always stay wall.
|
|
1048
|
+
*/
|
|
1049
|
+
export function isBuildingDoorOffset(dx, dz, footprint, doorFacing = 'posZ', halfW = 0) {
|
|
906
1050
|
const { radiusX, radiusZ } = normalizeBuildingFootprint(footprint);
|
|
907
1051
|
if (radiusX <= 0 || radiusZ <= 0)
|
|
908
1052
|
return false;
|
|
1053
|
+
const alongX = doorFacing === 'posZ' || doorFacing === 'negZ';
|
|
1054
|
+
const w = Math.max(0, Math.min(Math.floor(halfW), (alongX ? radiusX : radiusZ) - 1));
|
|
909
1055
|
if (doorFacing === 'posZ')
|
|
910
|
-
return dx
|
|
1056
|
+
return Math.abs(dx) <= w && dz === radiusZ;
|
|
911
1057
|
if (doorFacing === 'negZ')
|
|
912
|
-
return dx
|
|
1058
|
+
return Math.abs(dx) <= w && dz === -radiusZ;
|
|
913
1059
|
if (doorFacing === 'posX')
|
|
914
|
-
return dx === radiusX && dz
|
|
915
|
-
return dx === -radiusX && dz
|
|
1060
|
+
return dx === radiusX && Math.abs(dz) <= w;
|
|
1061
|
+
return dx === -radiusX && Math.abs(dz) <= w;
|
|
916
1062
|
}
|
|
917
|
-
export function buildingWallBlocksOffset(dx, dz, footprint, doorFacing = 'posZ') {
|
|
1063
|
+
export function buildingWallBlocksOffset(dx, dz, footprint, doorFacing = 'posZ', halfW = 0) {
|
|
918
1064
|
const { radiusX, radiusZ } = normalizeBuildingFootprint(footprint);
|
|
919
1065
|
if (radiusX <= 0 || radiusZ <= 0)
|
|
920
1066
|
return false;
|
|
@@ -923,15 +1069,21 @@ export function buildingWallBlocksOffset(dx, dz, footprint, doorFacing = 'posZ')
|
|
|
923
1069
|
const onWallRing = Math.abs(dx) === radiusX || Math.abs(dz) === radiusZ;
|
|
924
1070
|
if (!onWallRing)
|
|
925
1071
|
return false;
|
|
926
|
-
return !isBuildingDoorOffset(dx, dz, { radiusX, radiusZ }, doorFacing);
|
|
1072
|
+
return !isBuildingDoorOffset(dx, dz, { radiusX, radiusZ }, doorFacing, halfW);
|
|
927
1073
|
}
|
|
928
|
-
|
|
1074
|
+
/**
|
|
1075
|
+
* Inside the building for the roof hide: any footprint tile that is not a
|
|
1076
|
+
* blocked ring tile (the door, or the whole mouth, counts as inside). Pass
|
|
1077
|
+
* the mouth rule's `halfW` (buildingDoorHalfWidth(kind, style)) so the roof
|
|
1078
|
+
* lifts across a wide mouth exactly where the collision opens it.
|
|
1079
|
+
*/
|
|
1080
|
+
export function isBuildingInteriorOffset(dx, dz, footprint, doorFacing = 'posZ', halfW = 0) {
|
|
929
1081
|
const { radiusX, radiusZ } = normalizeBuildingFootprint(footprint);
|
|
930
1082
|
if (radiusX <= 0 || radiusZ <= 0)
|
|
931
1083
|
return false;
|
|
932
1084
|
if (Math.abs(dx) > radiusX || Math.abs(dz) > radiusZ)
|
|
933
1085
|
return false;
|
|
934
|
-
return !buildingWallBlocksOffset(dx, dz, { radiusX, radiusZ }, doorFacing);
|
|
1086
|
+
return !buildingWallBlocksOffset(dx, dz, { radiusX, radiusZ }, doorFacing, halfW);
|
|
935
1087
|
}
|
|
936
1088
|
export function boulderFootprintRadius(style, scale) {
|
|
937
1089
|
if (style !== 'boulder')
|
|
@@ -1199,6 +1351,97 @@ export function faceStyleDegrees(style) {
|
|
|
1199
1351
|
const deg = Number(face[1]);
|
|
1200
1352
|
return Number.isFinite(deg) ? deg : null;
|
|
1201
1353
|
}
|
|
1354
|
+
export function parseGardenWallRun(style) {
|
|
1355
|
+
const m = /^run-(\d+)-(ew|ns)(?:-(stone|hedge|noble|fence))?$/.exec(style ?? '');
|
|
1356
|
+
if (!m)
|
|
1357
|
+
return null;
|
|
1358
|
+
const len = Number(m[1]);
|
|
1359
|
+
if (!Number.isFinite(len) || len < 1)
|
|
1360
|
+
return null;
|
|
1361
|
+
return { len, alongX: m[2] === 'ew', look: m[3] ?? 'stone' };
|
|
1362
|
+
}
|
|
1363
|
+
export function gardenWallRunStyle(len, alongX, look = 'stone') {
|
|
1364
|
+
return `run-${Math.max(1, Math.floor(len))}-${alongX ? 'ew' : 'ns'}${look === 'stone' ? '' : `-${look}`}`;
|
|
1365
|
+
}
|
|
1366
|
+
/** The walk-through gate piece for a wall of `look`: noble walls take the iron gate. */
|
|
1367
|
+
export function gardenWallGateStyle(alongX, look = 'stone') {
|
|
1368
|
+
const token = look === 'noble' ? 'iron' : look === 'stone' ? '' : look;
|
|
1369
|
+
return `gate${token ? `-${token}` : ''}-${alongX ? 'ew' : 'ns'}`;
|
|
1370
|
+
}
|
|
1371
|
+
export function isGardenWallGateStyle(style) {
|
|
1372
|
+
return typeof style === 'string' && style.startsWith('gate');
|
|
1373
|
+
}
|
|
1374
|
+
/** The tiles a gardenwall blocks: a whole run, one piece, or nothing for a gate. */
|
|
1375
|
+
export function visitGardenWallOffsets(style, visit) {
|
|
1376
|
+
if (isGardenWallGateStyle(style))
|
|
1377
|
+
return;
|
|
1378
|
+
const run = parseGardenWallRun(style);
|
|
1379
|
+
if (!run) {
|
|
1380
|
+
visit(0, 0);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
for (let i = 0; i < run.len; i++) {
|
|
1384
|
+
if (run.alongX)
|
|
1385
|
+
visit(i, 0);
|
|
1386
|
+
else
|
|
1387
|
+
visit(0, i);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
/* ------------------------------------------------------------------ *
|
|
1391
|
+
* Tree groves (the Capital redesign, 2026-09-25): a clump of trees drawn as
|
|
1392
|
+
* ONE entity with one mesh per material, for the city's orchards, gardens
|
|
1393
|
+
* and verges. Style `grove-<3|5>[-<species>][-scenic]`: a 3 x 3 clump of
|
|
1394
|
+
* three trees or a 5 x 5 clump of five, about the entity's tile. Each trunk
|
|
1395
|
+
* stands on the tile TREE_GROVE_TRUNKS gives it (the frontend plants the
|
|
1396
|
+
* trees exactly there) and blocks that tile only; the canopy between is
|
|
1397
|
+
* walk-under, as it is round a single tree. A grove is never cut down
|
|
1398
|
+
* (woodwork.ts isCuttableTreeStyle).
|
|
1399
|
+
* ------------------------------------------------------------------ */
|
|
1400
|
+
export const TREE_GROVE_TRUNKS = {
|
|
1401
|
+
'grove-3': [[-1, -1], [1, -1], [0, 1]],
|
|
1402
|
+
'grove-5': [[-2, -2], [1, -2], [-1, 0], [2, 1], [-1, 2]],
|
|
1403
|
+
};
|
|
1404
|
+
/** The grove size a tree style names (3 or 5), or null for a single tree. */
|
|
1405
|
+
export function treeGroveSize(style) {
|
|
1406
|
+
if (!style)
|
|
1407
|
+
return null;
|
|
1408
|
+
if (style === 'grove-3' || style.startsWith('grove-3-'))
|
|
1409
|
+
return 3;
|
|
1410
|
+
if (style === 'grove-5' || style.startsWith('grove-5-'))
|
|
1411
|
+
return 5;
|
|
1412
|
+
return null;
|
|
1413
|
+
}
|
|
1414
|
+
/** The trunk tiles of a grove style, about its entity tile, or null for a single tree. */
|
|
1415
|
+
export function treeGroveTrunks(style) {
|
|
1416
|
+
const size = treeGroveSize(style);
|
|
1417
|
+
return size === null ? null : TREE_GROVE_TRUNKS[size === 3 ? 'grove-3' : 'grove-5'];
|
|
1418
|
+
}
|
|
1419
|
+
/*
|
|
1420
|
+
* Statue plinths (the Capital's monuments, 2026-09-25 review): a `statue`
|
|
1421
|
+
* whose style carries a `plinth3` or `plinth5` token stands on a stepped
|
|
1422
|
+
* stone plinth three or five tiles square about its tile, and the whole
|
|
1423
|
+
* square blocks (visitBlockedWalkOffsets). Style `<figure>[-plinth<3|5>][-<facing>]`,
|
|
1424
|
+
* e.g. `king-plinth5`, `knight-plinth3-w`: the figure token first (the asset
|
|
1425
|
+
* reads `king` / `knight` from it) and the facing last, so a statue without
|
|
1426
|
+
* the token is the one-tile statue it always was.
|
|
1427
|
+
*/
|
|
1428
|
+
export function statuePlinthSize(style) {
|
|
1429
|
+
const tokens = (style ?? '').split('-');
|
|
1430
|
+
if (tokens.includes('plinth5'))
|
|
1431
|
+
return 5;
|
|
1432
|
+
if (tokens.includes('plinth3'))
|
|
1433
|
+
return 3;
|
|
1434
|
+
return 0;
|
|
1435
|
+
}
|
|
1436
|
+
/** A statue style with its plinth token, the facing (if any) kept last. */
|
|
1437
|
+
export function statueStyleWithPlinth(style, size) {
|
|
1438
|
+
if (!size)
|
|
1439
|
+
return style;
|
|
1440
|
+
const tokens = style.split('-').filter((t) => t !== 'plinth3' && t !== 'plinth5');
|
|
1441
|
+
const last = tokens[tokens.length - 1];
|
|
1442
|
+
const facing = tokens.length > 1 && (last === 'n' || last === 's' || last === 'e' || last === 'w') ? tokens.pop() : undefined;
|
|
1443
|
+
return [...tokens, `plinth${size}`, ...(facing ? [facing] : [])].join('-');
|
|
1444
|
+
}
|
|
1202
1445
|
/**
|
|
1203
1446
|
* A work bench is a long table: 2.2 units along its local X and 1.0 deep at
|
|
1204
1447
|
* scale 1 (createWorkBenchAsset; the mine's is the same shape), so at the
|
|
@@ -1340,6 +1583,30 @@ export function visitBlockedWalkOffsets(kind, style, scale, visit, doorFacing =
|
|
|
1340
1583
|
}
|
|
1341
1584
|
return;
|
|
1342
1585
|
}
|
|
1586
|
+
if (kind === 'capitalbuilding') {
|
|
1587
|
+
// The Blender buildings (CAPITAL_BUILDING_MODELS). The facing always
|
|
1588
|
+
// comes from the style, whatever door facing the caller passed, so a
|
|
1589
|
+
// stamp without the door flag still agrees with the client.
|
|
1590
|
+
const model = capitalBuildingModel(style);
|
|
1591
|
+
if (!model) {
|
|
1592
|
+
visit(0, 0);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const fp = buildingFootprintExtents(kind, scale, style);
|
|
1596
|
+
const facing = stoneHouseDoorFacing(style);
|
|
1597
|
+
const halfW = buildingDoorHalfWidth(kind, style);
|
|
1598
|
+
for (let dz = -fp.radiusZ; dz <= fp.radiusZ; dz++) {
|
|
1599
|
+
for (let dx = -fp.radiusX; dx <= fp.radiusX; dx++) {
|
|
1600
|
+
if (model.solid || buildingWallBlocksOffset(dx, dz, fp, facing, halfW))
|
|
1601
|
+
visit(dx, dz);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
for (const [lx, lz] of model.frontProps ?? []) {
|
|
1605
|
+
const [dx, dz] = turnBuildingOffset(lx, lz, facing);
|
|
1606
|
+
visit(dx, dz);
|
|
1607
|
+
}
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1343
1610
|
if (kind === 'citywall') {
|
|
1344
1611
|
visitCityWallOffsets(style, visit);
|
|
1345
1612
|
return;
|
|
@@ -1349,11 +1616,18 @@ export function visitBlockedWalkOffsets(kind, style, scale, visit, doorFacing =
|
|
|
1349
1616
|
return;
|
|
1350
1617
|
}
|
|
1351
1618
|
if (kind === 'gardenwall') {
|
|
1352
|
-
|
|
1353
|
-
return;
|
|
1354
|
-
visit(0, 0);
|
|
1619
|
+
visitGardenWallOffsets(style, visit);
|
|
1355
1620
|
return;
|
|
1356
1621
|
}
|
|
1622
|
+
if (kind === 'tree') {
|
|
1623
|
+
// A grove blocks its trunks; a single tree falls through to its own tile.
|
|
1624
|
+
const trunks = treeGroveTrunks(style);
|
|
1625
|
+
if (trunks) {
|
|
1626
|
+
for (const [dx, dz] of trunks)
|
|
1627
|
+
visit(dx, dz);
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1357
1631
|
if (kind === 'fountain') {
|
|
1358
1632
|
// The basin: three by three about the centre.
|
|
1359
1633
|
for (let dz = -1; dz <= 1; dz++) {
|
|
@@ -1362,6 +1636,18 @@ export function visitBlockedWalkOffsets(kind, style, scale, visit, doorFacing =
|
|
|
1362
1636
|
}
|
|
1363
1637
|
return;
|
|
1364
1638
|
}
|
|
1639
|
+
if (kind === 'statue') {
|
|
1640
|
+
// A statue on a stepped plinth blocks the plinth's square; one without
|
|
1641
|
+
// a plinth token falls through to its own tile.
|
|
1642
|
+
const half = (statuePlinthSize(style) - 1) / 2;
|
|
1643
|
+
if (half > 0) {
|
|
1644
|
+
for (let dz = -half; dz <= half; dz++) {
|
|
1645
|
+
for (let dx = -half; dx <= half; dx++)
|
|
1646
|
+
visit(dx, dz);
|
|
1647
|
+
}
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1365
1651
|
if (kind === 'brokengate') {
|
|
1366
1652
|
// Posts and wreckage across three tiles; nobody climbs the splinters.
|
|
1367
1653
|
const acrossZ = frostSpanAcrossZ(style);
|
|
@@ -1387,6 +1673,29 @@ export function visitBlockedWalkOffsets(kind, style, scale, visit, doorFacing =
|
|
|
1387
1673
|
}
|
|
1388
1674
|
return;
|
|
1389
1675
|
}
|
|
1676
|
+
if (kind === 'marketstall') {
|
|
1677
|
+
// A stall is a counter two tiles wide under an awning, and the trailing
|
|
1678
|
+
// letter of the style says which way it fronts. Only its middle tile was
|
|
1679
|
+
// ever solid, so both ends of the counter were walk-through (James,
|
|
1680
|
+
// 2026-09-22: "I can walk through market stalls"). Hope Beach's coastal
|
|
1681
|
+
// stalls are the big ones, five along the counter with their barrels -
|
|
1682
|
+
// the same shape hopeBeach.ts already stamps on the server, now shared
|
|
1683
|
+
// with the client so its prediction agrees.
|
|
1684
|
+
const facing = typeof style === 'string' ? style.charAt(style.length - 1) : 's';
|
|
1685
|
+
const alongZ = facing === 'e' || facing === 'w';
|
|
1686
|
+
const coastal = typeof style === 'string' && style.startsWith('coastal-');
|
|
1687
|
+
const half = coastal ? 2 : 1;
|
|
1688
|
+
const deep = coastal ? 1 : 0;
|
|
1689
|
+
for (let a = -half; a <= half; a++) {
|
|
1690
|
+
for (let b = -deep; b <= deep; b++) {
|
|
1691
|
+
if (alongZ)
|
|
1692
|
+
visit(b, a);
|
|
1693
|
+
else
|
|
1694
|
+
visit(a, b);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1390
1699
|
if (kind === 'fence' && isFenceGateStyle(style)) {
|
|
1391
1700
|
if (isOpenGateStyle(style))
|
|
1392
1701
|
return;
|
|
@@ -1414,9 +1723,11 @@ export function visitBlockedWalkOffsets(kind, style, scale, visit, doorFacing =
|
|
|
1414
1723
|
if (buildingFp.radiusX > 0 && buildingFp.radiusZ > 0) {
|
|
1415
1724
|
// Hollow buildings: only the wall ring is solid. The origin is interior
|
|
1416
1725
|
// floor - blocking it sealed the storage hut so you could never stand inside.
|
|
1726
|
+
// The door is one tile, or a wider mouth by the mouth rule (a `mouth5` token).
|
|
1727
|
+
const mouthHalfW = buildingDoorHalfWidth(kind, style);
|
|
1417
1728
|
for (let dz = -buildingFp.radiusZ; dz <= buildingFp.radiusZ; dz++) {
|
|
1418
1729
|
for (let dx = -buildingFp.radiusX; dx <= buildingFp.radiusX; dx++) {
|
|
1419
|
-
if (!buildingWallBlocksOffset(dx, dz, buildingFp, doorFacing))
|
|
1730
|
+
if (!buildingWallBlocksOffset(dx, dz, buildingFp, doorFacing, mouthHalfW))
|
|
1420
1731
|
continue;
|
|
1421
1732
|
// Cottage is a walk-through: south porch door and north garden door.
|
|
1422
1733
|
if (kind === 'farmhut' &&
|