polats Claude Opus 4.8 (1M context) commited on
Commit
d649f36
·
1 Parent(s): f3cb070

Rebuild map bundle: Forgotten Plains grass/cliffs/props/stones

Browse files

web/mapSandbox.js re-bundled from auto-battler — picks up the Forgotten Plains
overhaul: plain-green grass base, 2.5D plateau cliffs (shared bakeCliffs), expanded
foliage + multi-tile props + water reeds, the full 3×4 tree, and the new stone-clump
system. No new assets (FP tile/prop sheets were already curated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. web/mapSandbox.js +209 -51
web/mapSandbox.js CHANGED
@@ -710,6 +710,31 @@ function centerTile([c0, r0], dNW, dNE, dSW, dSE) {
710
  if (dSW) return [c0, r0 + 4];
711
  return [c0 + 1, r0 + 4];
712
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
713
 
714
  // ../auto-battler/src/render/orcKingdom.js
715
  var ORC = "/assets/minifantasy/Minifantasy_Orc_Kingdom_v1.0/Minifantasy_Orc_Kingdom_Assets";
@@ -890,6 +915,15 @@ var FOREST_SCALE = 0.05;
890
  function forestField(seed, x, y) {
891
  return fbm(sub3(seed, 15740503), x * FOREST_SCALE, y * FOREST_SCALE);
892
  }
 
 
 
 
 
 
 
 
 
893
  var RIVER_SCALE = 0.022;
894
  var RIVER_WIDTH = 0.035;
895
  var WARP_SCALE2 = 0.03;
@@ -912,9 +946,9 @@ var FP_MOCKUP_URL = `${FP}/Minifantasy_ForgottenPlainsMockup.png`;
912
  var FP_MAP_ASSETS = [TILES2, SHADOW, PROPS, PROP_SHADOW];
913
  var TILE3 = 8;
914
  var CHUNK2 = 32;
915
- var GRASS_BASE = [2, 4];
916
- var GRASS_VARS = [[1, 1], [2, 1], [3, 1], [4, 1]];
917
- var GRASS_RATE = 5;
918
  var DIRT_BLOCK = [7, 3];
919
  var DIRT_FILL = [8, 4];
920
  var DIRT_VARS2 = [[7, 1], [8, 1], [9, 1]];
@@ -925,21 +959,54 @@ var WATER_BLOCK = [25, 3];
925
  var WATER_FILL = [26, 4];
926
  var WATER_VARS = [[26, 4]];
927
  var FILL_RATE = 6;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
928
  var FOLIAGE = [
929
- { tiles: [[9, 6], [10, 6], [11, 6]], weight: 4 },
930
- // grass tufts (most common)
931
- { tiles: [[9, 9], [10, 9], [11, 9]], weight: 2 },
932
- // flowers (red / purple / daisy)
933
- { tiles: [[12, 6], [13, 6], [14, 6]], weight: 1 }
934
  // small reeds
 
 
 
 
 
 
 
 
 
 
935
  ];
936
  var FOLIAGE_WEIGHT = FOLIAGE.reduce((s, g) => s + g.weight, 0);
937
  var FOLIAGE_RATE = 11;
 
 
938
  var TREES = [
939
  { frame: [155, 3, 21, 25], ax: 0.48, ay: 0.96 },
940
- // tree 1
941
- { frame: [155, 35, 21, 25], ax: 0.48, ay: 0.96 }
942
  // tree 2 (fruited)
 
 
943
  ];
944
  var TREE_SCALE_BASE = 1;
945
  var TREE_SCALE_JITTER = 0.14;
@@ -950,6 +1017,32 @@ var FOREST_BLOCK_Y = 2;
950
  var FOREST_THRESHOLD = 0.6;
951
  var FOREST_MIN_NB = 2;
952
  var FOREST_FILL = 0.5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
953
  var COL_GRASS2 = [97, 150, 55];
954
  var COL_DIRT2 = [118, 80, 38];
955
  var COL_STONE = [120, 120, 122];
@@ -1006,6 +1099,8 @@ function chunkFields2(seed, x0, y0) {
1006
  return cur;
1007
  };
1008
  const waterF = clean((x, y) => isRiver(seed, x, y));
 
 
1009
  const dirtRaw = clean((x, y) => isDirt(seed, x, y));
1010
  const stoneRaw = clean((x, y) => isStone(seed, x, y));
1011
  const fraw = new Uint8Array(SZ * SZ);
@@ -1017,6 +1112,15 @@ function chunkFields2(seed, x0, y0) {
1017
  for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) if ((di || dj) && fraw[idx(i + di, j + dj)]) c++;
1018
  forestRegion[idx(i, j)] = c >= FOREST_MIN_NB ? 1 : 0;
1019
  }
 
 
 
 
 
 
 
 
 
1020
  const dirt = new Uint8Array(SZ * SZ), stone = new Uint8Array(SZ * SZ);
1021
  for (let j = WATER_BUFFER; j < SZ - WATER_BUFFER; j++) for (let i = WATER_BUFFER; i < SZ - WATER_BUFFER; i++) {
1022
  let nearW = 0;
@@ -1024,10 +1128,11 @@ function chunkFields2(seed, x0, y0) {
1024
  nearW = 1;
1025
  break;
1026
  }
1027
- dirt[idx(i, j)] = dirtRaw[idx(i, j)] && !nearW ? 1 : 0;
1028
- stone[idx(i, j)] = stoneRaw[idx(i, j)] && !dirtRaw[idx(i, j)] && !nearW ? 1 : 0;
 
1029
  }
1030
- return { waterF, dirt, stone, forestRegion, M, SZ, x0, y0 };
1031
  }
1032
  var FP_GROUND_FILLS = [
1033
  { key: "grass", url: TILES2, tile: GRASS_BASE },
@@ -1078,11 +1183,12 @@ var fpConfig = (seed) => ({
1078
  // standalone map is unchanged; the multi-biome overworld passes a per-biome dither mask.
1079
  bake({ x0, y0, seed: seed2, ctx, tmp, Sprite, tex, texFrame, add, accept = () => true }) {
1080
  const f = chunkFields2(seed2, x0, y0);
1081
- const { waterF, dirt, stone, forestRegion, M, SZ } = f;
1082
  const idx = (i, j) => j * SZ + i;
1083
  const atW = (wx, wy) => waterF[idx(wx - x0 + M, wy - y0 + M)] === 1;
1084
  const atD = (wx, wy) => dirt[idx(wx - x0 + M, wy - y0 + M)] === 1;
1085
  const atS = (wx, wy) => stone[idx(wx - x0 + M, wy - y0 + M)] === 1;
 
1086
  const place = (coord, tx, ty) => {
1087
  add(ctx.tiles, coord[0], coord[1], tx, ty);
1088
  if (ctx.shadow && (!ctx.shadowSet || ctx.shadowSet.has(coord[0] + "," + coord[1]))) {
@@ -1117,10 +1223,23 @@ var fpConfig = (seed) => ({
1117
  if (atS(wx, wy)) blob(atS, STONE_BLOCK, STONE_FILL, STONE_VARS, 2, wx, wy, tx, ty);
1118
  if (atW(wx, wy)) blob(atW, WATER_BLOCK, WATER_FILL, WATER_VARS, 3, wx, wy, tx, ty);
1119
  }
 
 
 
 
 
 
 
 
 
1120
  if (ctx.props) for (let ty = 0; ty < CHUNK2; ty++) for (let tx = 0; tx < CHUNK2; tx++) {
1121
  if (!accept(tx, ty)) continue;
1122
  const wx = x0 + tx, wy = y0 + ty;
1123
- if (atW(wx, wy) || atD(wx, wy) || atS(wx, wy)) continue;
 
 
 
 
1124
  const h2 = hashU32(seed2 ^ 15733114, wx, wy);
1125
  if (h2 % FOLIAGE_RATE !== 0) continue;
1126
  let pick = (h2 >>> 8) % FOLIAGE_WEIGHT, g = FOLIAGE[0];
@@ -1131,13 +1250,17 @@ var fpConfig = (seed) => ({
1131
  }
1132
  pick -= grp.weight;
1133
  }
 
 
 
 
1134
  const [c, r] = g.tiles[(h2 >>> 16) % g.tiles.length];
1135
  const sp = new Sprite(texFrame(ctx.props, c * TILE3, r * TILE3, TILE3, TILE3));
1136
  sp.x = tx * TILE3;
1137
  sp.y = ty * TILE3;
1138
  tmp.addChild(sp);
1139
  }
1140
- const live = [];
1141
  if (ctx.props) {
1142
  const inGrove = (wx, wy) => forestRegion[idx(wx - x0 + M, wy - y0 + M)] === 1;
1143
  const bx0 = Math.floor(x0 / FOREST_BLOCK_X), bx1 = Math.floor((x0 + CHUNK2 - 1) / FOREST_BLOCK_X);
@@ -1150,6 +1273,13 @@ var fpConfig = (seed) => ({
1150
  if (!inGrove(wx, wy)) continue;
1151
  if ((h2 >>> 8 & 65535) / 65536 >= FOREST_FILL) continue;
1152
  if (atW(wx, wy) || atS(wx, wy)) continue;
 
 
 
 
 
 
 
1153
  const T = TREES[(h2 >>> 24) % TREES.length];
1154
  const flip = (h2 >>> 20 & 255) / 256 < TREE_FLIP_RATE;
1155
  const sc = TREE_SCALE_BASE * (1 + ((h2 >>> 12 & 255) / 256 - 0.5) * 2 * TREE_SCALE_JITTER);
@@ -1172,13 +1302,43 @@ var fpConfig = (seed) => ({
1172
  live.push({ sprite: tr, shadow });
1173
  }
1174
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1175
  return { meta: f, live };
1176
  },
1177
  macroColor(seed2, tx, ty) {
1178
- if (isRiver(seed2, tx, ty)) return COL_WATER;
1179
- if (isStone(seed2, tx, ty) && !isDirt(seed2, tx, ty)) return COL_STONE;
1180
- if (isDirt(seed2, tx, ty)) return COL_DIRT2;
1181
- return COL_GRASS2;
 
1182
  },
1183
  tileIndexAt(wx, wy, meta) {
1184
  if (!meta) return null;
@@ -1217,10 +1377,10 @@ var DARK_THRESHOLD = 0.6;
1217
  function isDark(seed, x, y) {
1218
  return fbm(sub4(seed, 55852), x * DARK_SCALE, y * DARK_SCALE) > DARK_THRESHOLD;
1219
  }
1220
- var ELEV_SCALE = 0.025;
1221
- var ELEV_THRESHOLD = 0.6;
1222
- function isRaised(seed, x, y) {
1223
- return fbm(sub4(seed, 57836), x * ELEV_SCALE, y * ELEV_SCALE) > ELEV_THRESHOLD;
1224
  }
1225
  var BONE_SCALE = 0.055;
1226
  var BONE_THRESHOLD = 0.62;
@@ -1295,6 +1455,26 @@ var FACE_VAR = [[[2, 16], [4, 16], [5, 16]], [[2, 17], [4, 17], [5, 17]]];
1295
  var FACE_L = [[1, 16], [1, 17]];
1296
  var FACE_R = [[6, 16], [6, 17]];
1297
  var CLIFF_SPARSE_RATE = 2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1298
  var CORRUPT_LIGHT = [1, 1];
1299
  var CORRUPT_DARK = [2, 1];
1300
  var NECRO_GROUND_FILLS = [
@@ -1327,7 +1507,7 @@ var COL_LIGHT = [130, 119, 136];
1327
  var COL_DARK = [105, 99, 113];
1328
  var COL_WATER2 = [74, 142, 48];
1329
  var COL_BONE = [180, 156, 126];
1330
- var COL_CLIFF = [175, 146, 109];
1331
  var COL_FOREST = [96, 101, 70];
1332
  var NECRO_LAYERS = [
1333
  ["m-bg", "Background", "ground"],
@@ -1377,12 +1557,12 @@ function boneCenterTile([c0, r0], dNW, dNE, dSW, dSE) {
1377
  return [c0 + 1, r0 + 1];
1378
  }
1379
  function biomeColor(seed, tx, ty) {
1380
- const raised = isRaised(seed, tx, ty);
1381
  if (!raised && isRiver2(seed, tx, ty)) return COL_WATER2;
1382
  if (isBone(seed, tx, ty)) return COL_BONE;
1383
  let c = isDark(seed, tx, ty) ? COL_DARK : COL_LIGHT;
1384
  if (forestField2(seed, tx, ty) > FOREST_THRESHOLD2) c = lerp3(c, COL_FOREST, 0.5);
1385
- if (raised) c = lerp3(c, COL_CLIFF, 0.22);
1386
  return c;
1387
  }
1388
  function loadImg2(url) {
@@ -1439,7 +1619,7 @@ function chunkFields3(seed, x0, y0) {
1439
  };
1440
  const darkRaw = clean((x, y) => isDark(seed, x, y));
1441
  const waterField = clean((x, y) => isRiver2(seed, x, y));
1442
- const raisedField = clean((x, y) => isRaised(seed, x, y));
1443
  const boneField = clean((x, y) => isBone(seed, x, y));
1444
  const fraw = new Uint8Array(SZ * SZ);
1445
  for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) fraw[idx(i, j)] = forestField2(seed, x0 + i - M, y0 + j - M) > FOREST_THRESHOLD2 ? 1 : 0;
@@ -1614,29 +1794,7 @@ var necropolisConfig = (seed) => ({
1614
  sp.y = ty * TILE4;
1615
  tmp.addChild(sp);
1616
  }
1617
- for (let ty = 0; ty < CHUNK3; ty++) for (let tx = 0; tx < CHUNK3; tx++) {
1618
- if (!accept(tx, ty)) continue;
1619
- const wx = x0 + tx, wy = y0 + ty;
1620
- if (isRaisedAt(wx, wy)) {
1621
- const bN = !isRaisedAt(wx, wy - 1), bE = !isRaisedAt(wx + 1, wy), bS = !isRaisedAt(wx, wy + 1), bW = !isRaisedAt(wx - 1, wy);
1622
- if (bN && bW) place(LIP_TL, tx, ty);
1623
- else if (bN && bE) place(LIP_TR, tx, ty);
1624
- else if (bS && bW) place(LIP_SW, tx, ty);
1625
- else if (bS && bE) place(LIP_SE, tx, ty);
1626
- else if (bN) place(sparse(LIP_T, LIP_T_VAR, wx, wy, 1, CLIFF_SPARSE_RATE, seed2), tx, ty);
1627
- else if (bS) place(sparse(LIP_S, LIP_S_VAR, wx, wy, 2, CLIFF_SPARSE_RATE, seed2), tx, ty);
1628
- else if (bW) place(sparse(WALL_L, WALL_L_VAR, wx, wy, 3, CLIFF_SPARSE_RATE, seed2), tx, ty);
1629
- else if (bE) place(sparse(WALL_R, WALL_R_VAR, wx, wy, 4, CLIFF_SPARSE_RATE, seed2), tx, ty);
1630
- } else {
1631
- for (let k = 1; k <= FACE_H; k++) {
1632
- if (isRaisedAt(wx, wy - k)) {
1633
- const leftEnd = !isRaisedAt(wx - 1, wy - k), rightEnd = !isRaisedAt(wx + 1, wy - k);
1634
- place(leftEnd ? FACE_L[k - 1] : rightEnd ? FACE_R[k - 1] : sparse(FACE[k - 1], FACE_VAR[k - 1], wx, wy, 5 + k, CLIFF_SPARSE_RATE, seed2), tx, ty);
1635
- break;
1636
- }
1637
- }
1638
- }
1639
- }
1640
  const live = [];
1641
  if (ctx.props) {
1642
  const nearWater = (wx, wy) => {
 
710
  if (dSW) return [c0, r0 + 4];
711
  return [c0 + 1, r0 + 4];
712
  }
713
+ function bakeCliffs(cfg, { chunk, x0, y0, seed, raised, place, accept = () => true }) {
714
+ for (let ty = 0; ty < chunk; ty++) for (let tx = 0; tx < chunk; tx++) {
715
+ if (!accept(tx, ty)) continue;
716
+ const wx = x0 + tx, wy = y0 + ty;
717
+ if (raised(wx, wy)) {
718
+ const bN = !raised(wx, wy - 1), bE = !raised(wx + 1, wy), bS = !raised(wx, wy + 1), bW = !raised(wx - 1, wy);
719
+ if (bN && bW) place(cfg.LIP_TL, tx, ty);
720
+ else if (bN && bE) place(cfg.LIP_TR, tx, ty);
721
+ else if (bS && bW) place(cfg.LIP_SW, tx, ty);
722
+ else if (bS && bE) place(cfg.LIP_SE, tx, ty);
723
+ else if (bN) place(sparse(cfg.LIP_T, cfg.LIP_T_VAR, wx, wy, 1, cfg.RATE, seed), tx, ty);
724
+ else if (bS) place(sparse(cfg.LIP_S, cfg.LIP_S_VAR, wx, wy, 2, cfg.RATE, seed), tx, ty);
725
+ else if (bW) place(sparse(cfg.WALL_L, cfg.WALL_L_VAR, wx, wy, 3, cfg.RATE, seed), tx, ty);
726
+ else if (bE) place(sparse(cfg.WALL_R, cfg.WALL_R_VAR, wx, wy, 4, cfg.RATE, seed), tx, ty);
727
+ } else {
728
+ for (let k = 1; k <= cfg.FACE_H; k++) {
729
+ if (raised(wx, wy - k)) {
730
+ const leftEnd = !raised(wx - 1, wy - k), rightEnd = !raised(wx + 1, wy - k);
731
+ place(leftEnd ? cfg.FACE_L[k - 1] : rightEnd ? cfg.FACE_R[k - 1] : sparse(cfg.FACE[k - 1], cfg.FACE_VAR[k - 1], wx, wy, 5 + k, cfg.RATE, seed), tx, ty);
732
+ break;
733
+ }
734
+ }
735
+ }
736
+ }
737
+ }
738
 
739
  // ../auto-battler/src/render/orcKingdom.js
740
  var ORC = "/assets/minifantasy/Minifantasy_Orc_Kingdom_v1.0/Minifantasy_Orc_Kingdom_Assets";
 
915
  function forestField(seed, x, y) {
916
  return fbm(sub3(seed, 15740503), x * FOREST_SCALE, y * FOREST_SCALE);
917
  }
918
+ var STONE_CLUMP_SCALE = 0.11;
919
+ function stoneClumpField(seed, x, y) {
920
+ return fbm(sub3(seed, 5702849), x * STONE_CLUMP_SCALE, y * STONE_CLUMP_SCALE);
921
+ }
922
+ var ELEV_SCALE = 0.025;
923
+ var ELEV_THRESHOLD = 0.6;
924
+ function isRaised(seed, x, y) {
925
+ return fbm(sub3(seed, 57836), x * ELEV_SCALE, y * ELEV_SCALE) > ELEV_THRESHOLD;
926
+ }
927
  var RIVER_SCALE = 0.022;
928
  var RIVER_WIDTH = 0.035;
929
  var WARP_SCALE2 = 0.03;
 
946
  var FP_MAP_ASSETS = [TILES2, SHADOW, PROPS, PROP_SHADOW];
947
  var TILE3 = 8;
948
  var CHUNK2 = 32;
949
+ var GRASS_BASE = [37, 11];
950
+ var GRASS_VARS = [...rect(1, 1, 4, 1), ...rect(2, 3, 3, 5)];
951
+ var GRASS_RATE = 9;
952
  var DIRT_BLOCK = [7, 3];
953
  var DIRT_FILL = [8, 4];
954
  var DIRT_VARS2 = [[7, 1], [8, 1], [9, 1]];
 
959
  var WATER_FILL = [26, 4];
960
  var WATER_VARS = [[26, 4]];
961
  var FILL_RATE = 6;
962
+ var FP_CLIFF = {
963
+ LIP_T: [25, 16],
964
+ LIP_T_VAR: [],
965
+ LIP_TL: [24, 16],
966
+ LIP_TR: [26, 16],
967
+ WALL_L: [24, 17],
968
+ WALL_L_VAR: [],
969
+ WALL_R: [26, 17],
970
+ WALL_R_VAR: [],
971
+ LIP_S: [25, 18],
972
+ LIP_S_VAR: [],
973
+ LIP_SW: [24, 18],
974
+ LIP_SE: [26, 18],
975
+ FACE_H: 2,
976
+ FACE: [[25, 19], [25, 20]],
977
+ FACE_VAR: [[], []],
978
+ FACE_L: [[24, 19], [24, 20]],
979
+ FACE_R: [[26, 19], [26, 20]],
980
+ RATE: 4
981
+ };
982
+ var COL_CLIFF = [150, 138, 112];
983
  var FOLIAGE = [
984
+ { weight: 5, tiles: [[9, 6], [10, 6], [11, 6]] },
985
+ // grass tufts
986
+ { weight: 3, tiles: [[12, 6], [13, 6]] },
 
 
987
  // small reeds
988
+ { weight: 3, tiles: [[9, 9], [10, 9], [11, 9], [12, 9], [13, 9]] },
989
+ // flowers (red/purple/daisy) + small grass
990
+ { weight: 1, sprite: { c: 14, r: 6, w: 1, h: 3 } },
991
+ // tall reed 1×3
992
+ { weight: 1, sprite: { c: 15, r: 6, w: 2, h: 3 } },
993
+ // wide reed 2×3
994
+ { weight: 1, sprite: { c: 17, r: 6, w: 2, h: 3 } },
995
+ // wide reed 2×3
996
+ { weight: 1, sprite: { c: 14, r: 9, w: 1, h: 2 } }
997
+ // cattail 1×2
998
  ];
999
  var FOLIAGE_WEIGHT = FOLIAGE.reduce((s, g) => s + g.weight, 0);
1000
  var FOLIAGE_RATE = 11;
1001
+ var WATER_FOLIAGE = { c: 15, r: 9, w: 1, h: 2 };
1002
+ var WATER_FOLIAGE_RATE = 16;
1003
  var TREES = [
1004
  { frame: [155, 3, 21, 25], ax: 0.48, ay: 0.96 },
1005
+ // tree 1 (plain)
1006
+ { frame: [155, 35, 21, 25], ax: 0.48, ay: 0.96 },
1007
  // tree 2 (fruited)
1008
+ { frame: [152, 32, 24, 32], ax: 0.5, ay: 0.94 }
1009
+ // tree 3 — full 3×4 fruited (19,4)
1010
  ];
1011
  var TREE_SCALE_BASE = 1;
1012
  var TREE_SCALE_JITTER = 0.14;
 
1017
  var FOREST_THRESHOLD = 0.6;
1018
  var FOREST_MIN_NB = 2;
1019
  var FOREST_FILL = 0.5;
1020
+ var STONES = [
1021
+ { c: 8, r: 3, w: 2, h: 2, weight: 3 },
1022
+ // small rock
1023
+ { c: 10, r: 3, w: 2, h: 2, weight: 3 },
1024
+ // small rock
1025
+ { c: 12, r: 3, w: 2, h: 2, weight: 3 },
1026
+ // small flat rock
1027
+ { c: 14, r: 3, w: 2, h: 2, weight: 3 },
1028
+ // small rock
1029
+ { c: 16, r: 3, w: 2, h: 2, weight: 3 },
1030
+ // small rock
1031
+ { c: 6, r: 3, w: 2, h: 3, weight: 2 },
1032
+ // medium boulder
1033
+ { c: 0, r: 3, w: 2, h: 4, weight: 1 },
1034
+ // big boulder
1035
+ { c: 2, r: 3, w: 2, h: 4, weight: 1 },
1036
+ // big boulder
1037
+ { c: 4, r: 3, w: 2, h: 4, weight: 1 }
1038
+ // big knobbly boulder
1039
+ ];
1040
+ var STONE_WEIGHT = STONES.reduce((s, g) => s + g.weight, 0);
1041
+ var STONE_BLOCK_X = 2;
1042
+ var STONE_BLOCK_Y = 2;
1043
+ var STONE_CLUMP_THRESHOLD = 0.7;
1044
+ var STONE_CLUMP_MIN_NB = 3;
1045
+ var STONE_CLUMP_FILL = 0.7;
1046
  var COL_GRASS2 = [97, 150, 55];
1047
  var COL_DIRT2 = [118, 80, 38];
1048
  var COL_STONE = [120, 120, 122];
 
1099
  return cur;
1100
  };
1101
  const waterF = clean((x, y) => isRiver(seed, x, y));
1102
+ const raisedField = clean((x, y) => isRaised(seed, x, y));
1103
+ for (let k = 0; k < waterF.length; k++) if (raisedField[k]) waterF[k] = 0;
1104
  const dirtRaw = clean((x, y) => isDirt(seed, x, y));
1105
  const stoneRaw = clean((x, y) => isStone(seed, x, y));
1106
  const fraw = new Uint8Array(SZ * SZ);
 
1112
  for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) if ((di || dj) && fraw[idx(i + di, j + dj)]) c++;
1113
  forestRegion[idx(i, j)] = c >= FOREST_MIN_NB ? 1 : 0;
1114
  }
1115
+ const sraw = new Uint8Array(SZ * SZ);
1116
+ for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) sraw[idx(i, j)] = stoneClumpField(seed, x0 + i - M, y0 + j - M) > STONE_CLUMP_THRESHOLD ? 1 : 0;
1117
+ const stoneClumpRegion = new Uint8Array(SZ * SZ);
1118
+ for (let j = 1; j < SZ - 1; j++) for (let i = 1; i < SZ - 1; i++) {
1119
+ if (!sraw[idx(i, j)]) continue;
1120
+ let c = 0;
1121
+ for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) if ((di || dj) && sraw[idx(i + di, j + dj)]) c++;
1122
+ stoneClumpRegion[idx(i, j)] = c >= STONE_CLUMP_MIN_NB ? 1 : 0;
1123
+ }
1124
  const dirt = new Uint8Array(SZ * SZ), stone = new Uint8Array(SZ * SZ);
1125
  for (let j = WATER_BUFFER; j < SZ - WATER_BUFFER; j++) for (let i = WATER_BUFFER; i < SZ - WATER_BUFFER; i++) {
1126
  let nearW = 0;
 
1128
  nearW = 1;
1129
  break;
1130
  }
1131
+ const rais = raisedField[idx(i, j)];
1132
+ dirt[idx(i, j)] = dirtRaw[idx(i, j)] && !nearW && !rais ? 1 : 0;
1133
+ stone[idx(i, j)] = stoneRaw[idx(i, j)] && !dirtRaw[idx(i, j)] && !nearW && !rais ? 1 : 0;
1134
  }
1135
+ return { waterF, dirt, stone, raisedField, forestRegion, stoneClumpRegion, M, SZ, x0, y0 };
1136
  }
1137
  var FP_GROUND_FILLS = [
1138
  { key: "grass", url: TILES2, tile: GRASS_BASE },
 
1183
  // standalone map is unchanged; the multi-biome overworld passes a per-biome dither mask.
1184
  bake({ x0, y0, seed: seed2, ctx, tmp, Sprite, tex, texFrame, add, accept = () => true }) {
1185
  const f = chunkFields2(seed2, x0, y0);
1186
+ const { waterF, dirt, stone, raisedField, forestRegion, stoneClumpRegion, M, SZ } = f;
1187
  const idx = (i, j) => j * SZ + i;
1188
  const atW = (wx, wy) => waterF[idx(wx - x0 + M, wy - y0 + M)] === 1;
1189
  const atD = (wx, wy) => dirt[idx(wx - x0 + M, wy - y0 + M)] === 1;
1190
  const atS = (wx, wy) => stone[idx(wx - x0 + M, wy - y0 + M)] === 1;
1191
+ const isRaisedAt = (wx, wy) => raisedField[idx(wx - x0 + M, wy - y0 + M)] === 1;
1192
  const place = (coord, tx, ty) => {
1193
  add(ctx.tiles, coord[0], coord[1], tx, ty);
1194
  if (ctx.shadow && (!ctx.shadowSet || ctx.shadowSet.has(coord[0] + "," + coord[1]))) {
 
1223
  if (atS(wx, wy)) blob(atS, STONE_BLOCK, STONE_FILL, STONE_VARS, 2, wx, wy, tx, ty);
1224
  if (atW(wx, wy)) blob(atW, WATER_BLOCK, WATER_FILL, WATER_VARS, 3, wx, wy, tx, ty);
1225
  }
1226
+ const live = [];
1227
+ const propSprite = (spec, wx, wy) => {
1228
+ const sp = new Sprite(texFrame(ctx.props, spec.c * TILE3, (spec.r - spec.h + 1) * TILE3, spec.w * TILE3, spec.h * TILE3));
1229
+ sp.anchor.set(0.5, 1);
1230
+ sp.x = wx * TILE3 + TILE3 / 2;
1231
+ sp.y = (wy + 1) * TILE3;
1232
+ sp.zIndex = (wy + 1) * TILE3;
1233
+ return sp;
1234
+ };
1235
  if (ctx.props) for (let ty = 0; ty < CHUNK2; ty++) for (let tx = 0; tx < CHUNK2; tx++) {
1236
  if (!accept(tx, ty)) continue;
1237
  const wx = x0 + tx, wy = y0 + ty;
1238
+ if (atW(wx, wy)) {
1239
+ if (hashU32(seed2 ^ 24301, wx, wy) % WATER_FOLIAGE_RATE === 0) live.push({ sprite: propSprite(WATER_FOLIAGE, wx, wy), shadow: null });
1240
+ continue;
1241
+ }
1242
+ if (atD(wx, wy) || atS(wx, wy)) continue;
1243
  const h2 = hashU32(seed2 ^ 15733114, wx, wy);
1244
  if (h2 % FOLIAGE_RATE !== 0) continue;
1245
  let pick = (h2 >>> 8) % FOLIAGE_WEIGHT, g = FOLIAGE[0];
 
1250
  }
1251
  pick -= grp.weight;
1252
  }
1253
+ if (g.sprite) {
1254
+ live.push({ sprite: propSprite(g.sprite, wx, wy), shadow: null });
1255
+ continue;
1256
+ }
1257
  const [c, r] = g.tiles[(h2 >>> 16) % g.tiles.length];
1258
  const sp = new Sprite(texFrame(ctx.props, c * TILE3, r * TILE3, TILE3, TILE3));
1259
  sp.x = tx * TILE3;
1260
  sp.y = ty * TILE3;
1261
  tmp.addChild(sp);
1262
  }
1263
+ bakeCliffs(FP_CLIFF, { chunk: CHUNK2, x0, y0, seed: seed2, raised: isRaisedAt, place, accept });
1264
  if (ctx.props) {
1265
  const inGrove = (wx, wy) => forestRegion[idx(wx - x0 + M, wy - y0 + M)] === 1;
1266
  const bx0 = Math.floor(x0 / FOREST_BLOCK_X), bx1 = Math.floor((x0 + CHUNK2 - 1) / FOREST_BLOCK_X);
 
1273
  if (!inGrove(wx, wy)) continue;
1274
  if ((h2 >>> 8 & 65535) / 65536 >= FOREST_FILL) continue;
1275
  if (atW(wx, wy) || atS(wx, wy)) continue;
1276
+ if (isRaisedAt(wx, wy)) continue;
1277
+ let onFace = false;
1278
+ for (let k = 1; k <= FP_CLIFF.FACE_H; k++) if (isRaisedAt(wx, wy - k)) {
1279
+ onFace = true;
1280
+ break;
1281
+ }
1282
+ if (onFace) continue;
1283
  const T = TREES[(h2 >>> 24) % TREES.length];
1284
  const flip = (h2 >>> 20 & 255) / 256 < TREE_FLIP_RATE;
1285
  const sc = TREE_SCALE_BASE * (1 + ((h2 >>> 12 & 255) / 256 - 0.5) * 2 * TREE_SCALE_JITTER);
 
1302
  live.push({ sprite: tr, shadow });
1303
  }
1304
  }
1305
+ if (ctx.props) {
1306
+ const inClump = (wx, wy) => stoneClumpRegion[idx(wx - x0 + M, wy - y0 + M)] === 1;
1307
+ const sbx0 = Math.floor(x0 / STONE_BLOCK_X), sbx1 = Math.floor((x0 + CHUNK2 - 1) / STONE_BLOCK_X);
1308
+ const sby0 = Math.floor(y0 / STONE_BLOCK_Y), sby1 = Math.floor((y0 + CHUNK2 - 1) / STONE_BLOCK_Y);
1309
+ for (let by = sby0; by <= sby1; by++) for (let bx = sbx0; bx <= sbx1; bx++) {
1310
+ const h2 = hashU32(seed2 ^ 5702885, bx, by);
1311
+ const wx = bx * STONE_BLOCK_X + h2 % STONE_BLOCK_X, wy = by * STONE_BLOCK_Y + (h2 >>> 4) % STONE_BLOCK_Y;
1312
+ if (wx < x0 || wx >= x0 + CHUNK2 || wy < y0 || wy >= y0 + CHUNK2) continue;
1313
+ if (!accept(wx - x0, wy - y0)) continue;
1314
+ if (!inClump(wx, wy)) continue;
1315
+ if ((h2 >>> 8 & 65535) / 65536 >= STONE_CLUMP_FILL) continue;
1316
+ if (atW(wx, wy) || isRaisedAt(wx, wy)) continue;
1317
+ let onFace = false;
1318
+ for (let k = 1; k <= FP_CLIFF.FACE_H; k++) if (isRaisedAt(wx, wy - k)) {
1319
+ onFace = true;
1320
+ break;
1321
+ }
1322
+ if (onFace) continue;
1323
+ let pick = (h2 >>> 20) % STONE_WEIGHT, st = STONES[0];
1324
+ for (const s of STONES) {
1325
+ if (pick < s.weight) {
1326
+ st = s;
1327
+ break;
1328
+ }
1329
+ pick -= s.weight;
1330
+ }
1331
+ live.push({ sprite: propSprite(st, wx, wy), shadow: null });
1332
+ }
1333
+ }
1334
  return { meta: f, live };
1335
  },
1336
  macroColor(seed2, tx, ty) {
1337
+ const raised = isRaised(seed2, tx, ty);
1338
+ if (!raised && isRiver(seed2, tx, ty)) return COL_WATER;
1339
+ let c = isStone(seed2, tx, ty) && !isDirt(seed2, tx, ty) ? COL_STONE : isDirt(seed2, tx, ty) ? COL_DIRT2 : COL_GRASS2;
1340
+ if (raised) c = lerp3(c, COL_CLIFF, 0.25);
1341
+ return c;
1342
  },
1343
  tileIndexAt(wx, wy, meta) {
1344
  if (!meta) return null;
 
1377
  function isDark(seed, x, y) {
1378
  return fbm(sub4(seed, 55852), x * DARK_SCALE, y * DARK_SCALE) > DARK_THRESHOLD;
1379
  }
1380
+ var ELEV_SCALE2 = 0.025;
1381
+ var ELEV_THRESHOLD2 = 0.6;
1382
+ function isRaised2(seed, x, y) {
1383
+ return fbm(sub4(seed, 57836), x * ELEV_SCALE2, y * ELEV_SCALE2) > ELEV_THRESHOLD2;
1384
  }
1385
  var BONE_SCALE = 0.055;
1386
  var BONE_THRESHOLD = 0.62;
 
1455
  var FACE_L = [[1, 16], [1, 17]];
1456
  var FACE_R = [[6, 16], [6, 17]];
1457
  var CLIFF_SPARSE_RATE = 2;
1458
+ var NECRO_CLIFF = {
1459
+ LIP_T,
1460
+ LIP_T_VAR,
1461
+ LIP_TL,
1462
+ LIP_TR,
1463
+ WALL_L,
1464
+ WALL_L_VAR,
1465
+ WALL_R,
1466
+ WALL_R_VAR,
1467
+ LIP_S,
1468
+ LIP_S_VAR,
1469
+ LIP_SW,
1470
+ LIP_SE,
1471
+ FACE_H,
1472
+ FACE,
1473
+ FACE_VAR,
1474
+ FACE_L,
1475
+ FACE_R,
1476
+ RATE: CLIFF_SPARSE_RATE
1477
+ };
1478
  var CORRUPT_LIGHT = [1, 1];
1479
  var CORRUPT_DARK = [2, 1];
1480
  var NECRO_GROUND_FILLS = [
 
1507
  var COL_DARK = [105, 99, 113];
1508
  var COL_WATER2 = [74, 142, 48];
1509
  var COL_BONE = [180, 156, 126];
1510
+ var COL_CLIFF2 = [175, 146, 109];
1511
  var COL_FOREST = [96, 101, 70];
1512
  var NECRO_LAYERS = [
1513
  ["m-bg", "Background", "ground"],
 
1557
  return [c0 + 1, r0 + 1];
1558
  }
1559
  function biomeColor(seed, tx, ty) {
1560
+ const raised = isRaised2(seed, tx, ty);
1561
  if (!raised && isRiver2(seed, tx, ty)) return COL_WATER2;
1562
  if (isBone(seed, tx, ty)) return COL_BONE;
1563
  let c = isDark(seed, tx, ty) ? COL_DARK : COL_LIGHT;
1564
  if (forestField2(seed, tx, ty) > FOREST_THRESHOLD2) c = lerp3(c, COL_FOREST, 0.5);
1565
+ if (raised) c = lerp3(c, COL_CLIFF2, 0.22);
1566
  return c;
1567
  }
1568
  function loadImg2(url) {
 
1619
  };
1620
  const darkRaw = clean((x, y) => isDark(seed, x, y));
1621
  const waterField = clean((x, y) => isRiver2(seed, x, y));
1622
+ const raisedField = clean((x, y) => isRaised2(seed, x, y));
1623
  const boneField = clean((x, y) => isBone(seed, x, y));
1624
  const fraw = new Uint8Array(SZ * SZ);
1625
  for (let j = 0; j < SZ; j++) for (let i = 0; i < SZ; i++) fraw[idx(i, j)] = forestField2(seed, x0 + i - M, y0 + j - M) > FOREST_THRESHOLD2 ? 1 : 0;
 
1794
  sp.y = ty * TILE4;
1795
  tmp.addChild(sp);
1796
  }
1797
+ bakeCliffs(NECRO_CLIFF, { chunk: CHUNK3, x0, y0, seed: seed2, raised: isRaisedAt, place, accept });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1798
  const live = [];
1799
  if (ctx.props) {
1800
  const nearWater = (wx, wy) => {