Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16efccd912 | ||
|
|
96815f6685 | ||
|
|
7269ff53f0 |
No files matched your search
+3
-3
@@ -4,7 +4,7 @@ This guide consolidates the durable project knowledge used to develop, test,
|
||||
package, and maintain MeatSlicer. When this guide and the implementation
|
||||
disagree, the implementation and configuration files are the source of truth.
|
||||
|
||||
**Memory version: 0.5.2**
|
||||
**Memory version: 0.6.0**
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -201,8 +201,8 @@ failures, and an SFX buffer count matching `SfxBank.FILES` and the files on
|
||||
disk; the exact count can change as audio is added.
|
||||
|
||||
The Windows build is not code-signed, so downloaded copies may trigger a
|
||||
SmartScreen warning. Code signing, installers, automatic updates, and
|
||||
macOS/Linux packaging remain future work.
|
||||
SmartScreen warning. Code signing, installers, and automatic updates remain
|
||||
future work.
|
||||
|
||||
## Visual asset pipeline
|
||||
|
||||
|
||||
+22
-22
@@ -6,32 +6,32 @@
|
||||
<meta name="theme-color" content="#0a0305">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 64 64%22><text y=%2252%22 x=%2250%22 text-anchor=%22middle%22 dominant-baseline=%22middle%22 font-size=%2252%22>🔪</text></svg>">
|
||||
<title>MEATSLICER</title>
|
||||
<link rel="stylesheet" href="css/style.css?v=51">
|
||||
<link rel="stylesheet" href="css/style.css?v=52">
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<canvas id="game" width="960" height="640"></canvas>
|
||||
</div>
|
||||
<script src="js/utils.js?v=51"></script>
|
||||
<script src="js/state.js?v=51"></script>
|
||||
<script src="js/input.js?v=51"></script>
|
||||
<script src="js/sfx.js?v=51"></script>
|
||||
<script src="js/sfxbank.js?v=51"></script>
|
||||
<script src="js/audio.js?v=51"></script>
|
||||
<script src="js/sprites.js?v=51"></script>
|
||||
<script src="js/particles.js?v=51"></script>
|
||||
<script src="js/weapons.js?v=51"></script>
|
||||
<script src="js/bullets.js?v=51"></script>
|
||||
<script src="js/pickups.js?v=51"></script>
|
||||
<script src="js/items.js?v=51"></script>
|
||||
<script src="js/perks.js?v=51"></script>
|
||||
<script src="js/enemies.js?v=51"></script>
|
||||
<script src="js/bosses.js?v=51"></script>
|
||||
<script src="js/rooms.js?v=51"></script>
|
||||
<script src="js/player.js?v=51"></script>
|
||||
<script src="js/hud.js?v=51"></script>
|
||||
<script src="js/help.js?v=51"></script>
|
||||
<script src="js/debug.js?v=51"></script>
|
||||
<script src="js/main.js?v=51"></script>
|
||||
<script src="js/utils.js?v=52"></script>
|
||||
<script src="js/state.js?v=52"></script>
|
||||
<script src="js/input.js?v=52"></script>
|
||||
<script src="js/sfx.js?v=52"></script>
|
||||
<script src="js/sfxbank.js?v=52"></script>
|
||||
<script src="js/audio.js?v=52"></script>
|
||||
<script src="js/sprites.js?v=52"></script>
|
||||
<script src="js/particles.js?v=52"></script>
|
||||
<script src="js/weapons.js?v=52"></script>
|
||||
<script src="js/bullets.js?v=52"></script>
|
||||
<script src="js/pickups.js?v=52"></script>
|
||||
<script src="js/items.js?v=52"></script>
|
||||
<script src="js/perks.js?v=52"></script>
|
||||
<script src="js/enemies.js?v=52"></script>
|
||||
<script src="js/bosses.js?v=52"></script>
|
||||
<script src="js/rooms.js?v=52"></script>
|
||||
<script src="js/player.js?v=52"></script>
|
||||
<script src="js/hud.js?v=52"></script>
|
||||
<script src="js/help.js?v=52"></script>
|
||||
<script src="js/debug.js?v=52"></script>
|
||||
<script src="js/main.js?v=52"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-2
@@ -72,7 +72,7 @@ function dbgClicked(rect) { return Input.mpressed && inRect(Input.mx, Input.my,
|
||||
// clamp the cursor to the arena so spawned pickups/pedestals never land in walls
|
||||
function dbgCursor() {
|
||||
const a = G.arena;
|
||||
return { x: clamp(Input.mx, a.x0 + 12, a.x1 - 12), y: clamp(Input.my, a.y0 + 12, a.y1 - 12) };
|
||||
return { x: clamp(mxW(), a.x0 + 12, a.x1 - 12), y: clamp(myW(), a.y0 + 12, a.y1 - 12) };
|
||||
}
|
||||
|
||||
// number row: label + [-] value [+]
|
||||
@@ -185,7 +185,7 @@ function dbgRenderPlayer(ctx, b) {
|
||||
{ t: 'KILL PLAYER', fn: () => { p.hp = 0; gameOver(); } },
|
||||
{ t: '+1 LEVELUP', fn: () => { G.pendingLevelups++; addToast('DEBUG', '+1 pending levelup'); } },
|
||||
{ t: 'OPEN DRAFT', fn: () => { openPerkDraft(); } },
|
||||
{ t: 'TP TO CURSOR', fn: () => { p.x = clamp(Input.mx, G.arena.x0 + p.r, G.arena.x1 - p.r); p.y = clamp(Input.my, G.arena.y0 + p.r, G.arena.y1 - p.r); } },
|
||||
{ t: 'TP TO CURSOR', fn: () => { p.x = clamp(mxW(), G.arena.x0 + p.r, G.arena.x1 - p.r); p.y = clamp(myW(), G.arena.y0 + p.r, G.arena.y1 - p.r); } },
|
||||
{ t: '+500 SCORE', fn: () => addScore(500) },
|
||||
];
|
||||
for (let i = 0; i < acts.length; i++) {
|
||||
|
||||
+1
-1
@@ -198,7 +198,7 @@ const ACTIVES = {
|
||||
spawnShockwave(p.x, p.y, 150, '#e8dcc2', 0.8); addShake(8);
|
||||
} },
|
||||
offalbomb: { name: 'Offal Bomb', desc: 'Gore bomb at the cursor', cost: 1, use(p) {
|
||||
explodeAt(Input.mx, Input.my, 95, 25 * p.stats.dmgMul, true);
|
||||
explodeAt(mxW(), myW(), 95, 25 * p.stats.dmgMul, true);
|
||||
} },
|
||||
bloodtransfusion:{ name: 'Blood Transfusion', desc: 'Heal 2 hearts, lose 25% current XP', cost: 2, use(p) {
|
||||
p.xp = Math.floor(p.xp * 0.75);
|
||||
|
||||
+6
-2
@@ -298,8 +298,11 @@ function draw() {
|
||||
|
||||
if (G.mode === 'menu') { drawMenu(ctx); return; }
|
||||
|
||||
// world (with screen shake)
|
||||
updateCamera();
|
||||
|
||||
// world (with camera + screen shake)
|
||||
ctx.save();
|
||||
ctx.translate(-G.cam.x, -G.cam.y);
|
||||
if (G.shake > 0) ctx.translate(rand(-G.shake, G.shake) * 0.5, rand(-G.shake, G.shake) * 0.5);
|
||||
drawRoom(ctx);
|
||||
drawCorpses(ctx);
|
||||
@@ -309,6 +312,7 @@ function draw() {
|
||||
drawBullets(ctx);
|
||||
drawParticles(ctx);
|
||||
drawTelegraphs(ctx);
|
||||
if (G.devMode) drawDebugOverlays(ctx);
|
||||
ctx.restore();
|
||||
|
||||
if (G.atmosphereLayer) ctx.drawImage(G.atmosphereLayer, 0, 0);
|
||||
@@ -340,7 +344,7 @@ function draw() {
|
||||
else if (G.mode === 'pause') { drawPause(ctx); if (G.pauseHelp) drawPauseHelp(ctx); }
|
||||
else if (G.mode === 'gameover') drawGameOver(ctx);
|
||||
else if (G.mode === 'debug') drawDebug(ctx);
|
||||
if (G.devMode) { drawDebugOverlays(ctx); if (G.debugPin && G.mode !== 'debug') drawDebugPin(ctx); }
|
||||
if (G.devMode) { if (G.debugPin && G.mode !== 'debug') drawDebugPin(ctx); }
|
||||
}
|
||||
|
||||
function drawMenu(ctx) {
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ function updatePlayer(dt) {
|
||||
p.y = clamp(p.y, G.arena.y0 + p.r, G.arena.y1 - p.r);
|
||||
|
||||
// aim
|
||||
p.aim = angleTo(p.x, p.y, Input.mx, Input.my);
|
||||
p.aim = angleTo(p.x, p.y, mxW(), myW());
|
||||
// The waist and legs turn smoothly with travel while the weapon torso aims freely.
|
||||
// Once planted, the lower body turns back under the independently aimed torso.
|
||||
if (mx || my) {
|
||||
|
||||
+31
-11
@@ -3,11 +3,15 @@
|
||||
const DIRS = { n: [0, -1], s: [0, 1], e: [1, 0], w: [-1, 0] };
|
||||
const OPP = { n: 's', s: 'n', e: 'w', w: 'e' };
|
||||
const ROOM_SHAPES = {
|
||||
hall: { x0: 48, y0: 48, x1: 912, y1: 592 },
|
||||
wide_hall: { x0: 48, y0: 208, x1: 912, y1: 432 },
|
||||
tall_hall: { x0: 368, y0: 48, x1: 592, y1: 592 },
|
||||
chamber: { x0: 208, y0: 128, x1: 752, y1: 512 },
|
||||
pit: { x0: 128, y0: 88, x1: 832, y1: 552 },
|
||||
hall: { x0: 48, y0: 48, x1: 912, y1: 592 }, // unchanged — start & boss rooms
|
||||
wide_hall: { x0: 48, y0: 176, x1: 912, y1: 464 }, // h 224 -> 288 ("a little wider"; cy stays 320, min dim 288 < 300 keeps censer/broodsac excluded)
|
||||
tall_hall: { x0: 336, y0: 48, x1: 624, y1: 592 }, // w 224 -> 288 (cx stays 480)
|
||||
chamber: { x0: 208, y0: 128, x1: 752, y1: 512 }, // unchanged — item rooms
|
||||
pit: { x0: 128, y0: 88, x1: 832, y1: 552 }, // unchanged
|
||||
grand_hall:{ x0: 48, y0: 48, x1: 1680, y1: 592 }, // new, ~1.7× wide — camera scrolls horizontally (w 1632 > 960)
|
||||
deep_hall: { x0: 480, y0: 48, x1: 912, y1: 1136 }, // new, ~1.85× tall — camera scrolls vertically (h 1088 > 640)
|
||||
meat_hall: { x0: 48, y0: 48, x1: 1680, y1: 1136 }, // new, 1.7× × 1.85× — camera scrolls both axes
|
||||
odd_hall: { x0: 160, y0: 128, x1: 800, y1: 512 }, // new, fits screen but off-center — visually "odd" with zero camera
|
||||
};
|
||||
const ROOM_THEMES = {
|
||||
abattoir: { floor: ['tile_floor1', 'tile_floor1', 'tile_floor4', 'tile_floor6'], wall: ['tile_wall', 'tile_wall2'] },
|
||||
@@ -49,13 +53,14 @@ function genFloor(floor) {
|
||||
const d = Math.abs(r.gx) + Math.abs(r.gy);
|
||||
if (r.type === 'combat' && d >= 2) { r.type = 'item'; break; }
|
||||
}
|
||||
const combatShapes = ['hall', 'hall', 'hall', 'pit', 'chamber', 'wide_hall', 'tall_hall'];
|
||||
const smallShapes = ['hall', 'hall', 'pit', 'chamber', 'wide_hall', 'tall_hall', 'odd_hall'];
|
||||
const bigShapes = ['grand_hall', 'deep_hall', 'meat_hall'];
|
||||
const themes = Object.keys(ROOM_THEMES);
|
||||
for (const r of Object.values(G.rooms)) {
|
||||
r.theme = choice(themes);
|
||||
if (r.type === 'start' || r.type === 'boss') r.shape = 'hall';
|
||||
else if (r.type === 'item') r.shape = 'chamber';
|
||||
else r.shape = choice(combatShapes);
|
||||
else r.shape = (G.floor >= 2 && chance(0.30)) ? choice(bigShapes) : choice(smallShapes);
|
||||
for (const [dir, [dx, dy]] of Object.entries(DIRS)) {
|
||||
r.doors[dir] = !!G.rooms[(r.gx + dx) + ',' + (r.gy + dy)];
|
||||
}
|
||||
@@ -143,6 +148,20 @@ function enterRoom(gx, gy) {
|
||||
}
|
||||
}
|
||||
|
||||
// Isaac-style: the view centers on the player and is clamped so it never leaves
|
||||
// the room. Rooms that fit the screen keep cam exactly at the room center —
|
||||
// which is (0,0) for every current shape, so existing rendering is byte-identical.
|
||||
function updateCamera() {
|
||||
const a = G.arena, p = G.player;
|
||||
if (!a || !p) return;
|
||||
G.cam = G.cam || { x: 0, y: 0 };
|
||||
G.cam.x = a.w <= W ? a.cx - W / 2 : clamp(p.x - W / 2, a.x0, a.x1 - W);
|
||||
G.cam.y = a.h <= H ? a.cy - H / 2 : clamp(p.y - H / 2, a.y0, a.y1 - H);
|
||||
}
|
||||
// screen-space mouse -> world coords (safe pre-player: cam exists from state.js)
|
||||
function mxW() { return Input.mx + G.cam.x; }
|
||||
function myW() { return Input.my + G.cam.y; }
|
||||
|
||||
function separateEntryWave() {
|
||||
const p = G.player;
|
||||
const a = G.arena;
|
||||
@@ -338,11 +357,11 @@ const FLOOR_TILE_POOL = [
|
||||
];
|
||||
const WALL_TILE_POOL = ['tile_wall', 'tile_wall', 'tile_wall2', 'tile_wall3', 'tile_wall4'];
|
||||
|
||||
function drawFloorTiles(target, r) {
|
||||
function drawFloorTiles(target, r, extW, extH) {
|
||||
const a = roomBounds(r);
|
||||
const theme = ROOM_THEMES[r.theme] || ROOM_THEMES.abattoir;
|
||||
target.fillStyle = '#050204';
|
||||
target.fillRect(0, 0, W, H);
|
||||
target.fillRect(0, 0, extW === undefined ? W : extW, extH === undefined ? H : extH);
|
||||
target.save();
|
||||
target.beginPath(); target.rect(a.x0, a.y0, a.w, a.h); target.clip();
|
||||
const tx0 = Math.floor(a.x0 / TILE), tx1 = Math.ceil(a.x1 / TILE);
|
||||
@@ -364,11 +383,12 @@ function drawFloor(ctx, r) {
|
||||
if (!canCache) { drawFloorTiles(ctx, r); return; }
|
||||
const key = r.gx + ',' + r.gy + ':' + G.floor + ':' + r.shape + ':' + r.theme;
|
||||
if (!G.roomLayer || G.roomLayerKey !== key) {
|
||||
const a = roomBounds(r);
|
||||
const layer = document.createElement('canvas');
|
||||
layer.width = W; layer.height = H;
|
||||
layer.width = Math.max(W, a.x1); layer.height = Math.max(H, a.y1);
|
||||
const lctx = layer.getContext('2d');
|
||||
lctx.imageSmoothingEnabled = false;
|
||||
drawFloorTiles(lctx, r);
|
||||
drawFloorTiles(lctx, r, layer.width, layer.height);
|
||||
G.roomLayer = layer; G.roomLayerKey = key;
|
||||
}
|
||||
ctx.drawImage(G.roomLayer, 0, 0);
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ const SfxBank = {
|
||||
if (this.buffers.has(name) || this.pending.has(name) || this.failed.has(name)) return;
|
||||
const file = this.FILES[name];
|
||||
if (!file || !this.ctx || typeof fetch !== 'function') return;
|
||||
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=51')
|
||||
const job = fetch('/assets/sfx/' + encodeURIComponent(file) + '?v=52')
|
||||
.then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.arrayBuffer(); })
|
||||
.then(bytes => this.ctx.decodeAudioData(bytes))
|
||||
.then(buffer => this.buffers.set(name, this.trim(buffer)))
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ const Sprites = {
|
||||
const img = new Image();
|
||||
img.onload = () => { this.imgs[name] = img; if (--pending === 0) G.imagesLoaded = true; };
|
||||
img.onerror = () => { if (--pending === 0) G.imagesLoaded = true; };
|
||||
img.src = 'assets/' + name + '.png?v=51';
|
||||
img.src = 'assets/' + name + '.png?v=52';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ const G = {
|
||||
roomLayerKey: '',
|
||||
atmosphereLayer: null,
|
||||
arena: { x0: WALL, y0: WALL, x1: W - WALL, y1: H - WALL, cx: W / 2, cy: H / 2, w: W - WALL * 2, h: H - WALL * 2 },
|
||||
cam: { x: 0, y: 0 }, // world coords of the viewport's top-left; 0,0 for every fit-to-screen room
|
||||
};
|
||||
|
||||
const PERK_POWER_WEIGHT = 0.5;
|
||||
|
||||
+3
-3
@@ -100,11 +100,11 @@ function fireWeapon(p, w) {
|
||||
break;
|
||||
}
|
||||
case 'lob': case 'lob_trap': case 'lob_swarm': {
|
||||
const primaryD = Math.max(dist(bx, by, Input.mx, Input.my), 1);
|
||||
const primaryD = Math.max(dist(bx, by, mxW(), myW()), 1);
|
||||
for (let i = 0; i < volley.length; i++) {
|
||||
const a = volley[i];
|
||||
const tx = i === 0 ? Input.mx : p.x + Math.cos(a) * primaryD;
|
||||
const ty = i === 0 ? Input.my : p.y + Math.sin(a) * primaryD;
|
||||
const tx = i === 0 ? mxW() : p.x + Math.cos(a) * primaryD;
|
||||
const ty = i === 0 ? myW() : p.y + Math.sin(a) * primaryD;
|
||||
const d = Math.max(dist(bx, by, tx, ty), 1);
|
||||
const flight = clamp(d / (520 * spdMul), 0.12, 0.8);
|
||||
mk(a, {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meatslicer",
|
||||
"productName": "MeatSlicer",
|
||||
"version": "0.5.2",
|
||||
"version": "0.6.0",
|
||||
"private": true,
|
||||
"description": "A Butcher's Descent - clear, harvest, descend.",
|
||||
"main": "desktop/main.js",
|
||||
|
||||
+59
-1
@@ -61,7 +61,7 @@ for (const f of files) {
|
||||
}
|
||||
// const/let top-level bindings live in the context's lexical scope, not on the
|
||||
// global object — expose the ones the harness pokes at directly.
|
||||
vm.runInContext('Object.assign(this, { G, Input, WEAPONS, Music, Sfx, SfxBank, W, H, WALL, ITEMS, ACTIVES, PERKS, ENEMY_TYPES, BOSS_DEFS, SPRITE_MANIFEST, Sprites, ACTOR_ANIMS, PRESSURE_UNIT, PRESSURE_MIN, PRESSURE_MAX, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX, ROOM_SHAPES, ROOM_THEMES, HELP_PAGES, HELP_RENDERERS, SPAWN_WARN, ENTRY_WARN, ENTRY_BUFFER, STUN_UNIT, stunChance, separateEntryWave, defaultPlayerStats, applyPressureDelta, debugRebuildStats, debugEnabled, DEBUG_PAGES })', sandbox);
|
||||
vm.runInContext('Object.assign(this, { G, Input, WEAPONS, Music, Sfx, SfxBank, W, H, WALL, ITEMS, ACTIVES, PERKS, ENEMY_TYPES, BOSS_DEFS, SPRITE_MANIFEST, Sprites, ACTOR_ANIMS, PRESSURE_UNIT, PRESSURE_MIN, PRESSURE_MAX, PRESSURE_DIAL_MIN, PRESSURE_DIAL_MAX, ROOM_SHAPES, ROOM_THEMES, HELP_PAGES, HELP_RENDERERS, SPAWN_WARN, ENTRY_WARN, ENTRY_BUFFER, STUN_UNIT, stunChance, separateEntryWave, defaultPlayerStats, applyPressureDelta, debugRebuildStats, debugEnabled, DEBUG_PAGES, updateCamera, mxW, myW, angleTo, genFloor, roomBounds })', sandbox);
|
||||
|
||||
let simTime = 0;
|
||||
const ctx = sandbox;
|
||||
@@ -279,6 +279,64 @@ console.log('== variable room shapes and themes ==');
|
||||
ctx.G.cur = savedRoom; ctx.G.arena = savedArena; ctx.G.player.x = px; ctx.G.player.y = py;
|
||||
}
|
||||
|
||||
console.log('== big rooms + camera ==');
|
||||
{
|
||||
// fit rooms keep camera pinned at (0,0) — current look is unchanged
|
||||
const fit = ctx.makeRoom(0, 0, 'combat'); fit.shape = 'hall';
|
||||
ctx.setArenaForRoom(fit);
|
||||
ctx.G.player.x = 100; ctx.G.player.y = 320;
|
||||
ctx.updateCamera();
|
||||
check('fit rooms keep camera at origin', ctx.G.cam.x === 0 && ctx.G.cam.y === 0);
|
||||
|
||||
// wide room: camera follows the player, clamped to the room
|
||||
const wide = ctx.makeRoom(0, 0, 'combat'); wide.shape = 'grand_hall';
|
||||
ctx.setArenaForRoom(wide);
|
||||
const wa = ctx.G.arena;
|
||||
ctx.G.player.x = 100; ctx.G.player.y = 320; ctx.updateCamera();
|
||||
check('camera clamps at left arena edge', ctx.G.cam.x === wa.x0);
|
||||
ctx.G.player.x = wa.x1 - 200; ctx.G.player.y = 320; ctx.updateCamera();
|
||||
check('camera clamps at right arena edge', ctx.G.cam.x === wa.x1 - ctx.W);
|
||||
ctx.G.player.x = wa.cx; ctx.G.player.y = 320; ctx.updateCamera();
|
||||
check('camera centers on the player mid-room', ctx.G.cam.x === wa.cx - ctx.W / 2 && ctx.G.cam.y === 0);
|
||||
|
||||
// world-mouse conversion
|
||||
ctx.G.cam.x = wa.cx - ctx.W / 2; ctx.Input.mx = 120; ctx.Input.my = 240;
|
||||
check('screen mouse maps to world coords', ctx.mxW() === 120 + ctx.G.cam.x && ctx.myW() === 240);
|
||||
|
||||
// end-to-end: live in a big room — update+draw path, floor direct path, aim
|
||||
ctx.startRun();
|
||||
const big = ctx.G.rooms['0,0'];
|
||||
big.shape = 'meat_hall';
|
||||
ctx.G.cur = big; ctx.setArenaForRoom(big);
|
||||
ctx.G.player.x = ctx.G.arena.x0 + 80; ctx.G.player.y = ctx.G.arena.y0 + 80;
|
||||
ctx.G.enemies.length = 0;
|
||||
ctx.Input.mx = 300; ctx.Input.my = 200; ctx.Input.mdown = true;
|
||||
step(3, 16); // runs update() (firing, weapon lob path) + draw() (direct floor, camera)
|
||||
check('big room runs without error and aims at world mouse',
|
||||
Math.abs(ctx.G.player.aim - ctx.angleTo(ctx.G.player.x, ctx.G.player.y, ctx.Input.mx + ctx.G.cam.x, ctx.Input.my + ctx.G.cam.y)) < 1e-9);
|
||||
ctx.Input.mdown = false;
|
||||
ctx.startRun(); step(3, 16);
|
||||
}
|
||||
|
||||
// floor >= 2 mixes big/odd rooms into combat shapes; doors stay reciprocal
|
||||
{
|
||||
ctx.G.floor = 6;
|
||||
let sawBig = false, reciprocal = true;
|
||||
for (let trial = 0; trial < 40 && !sawBig; trial++) {
|
||||
ctx.genFloor(6);
|
||||
for (const r of Object.values(ctx.G.rooms)) {
|
||||
if (r.type === 'combat' && ['grand_hall', 'deep_hall', 'meat_hall', 'odd_hall'].includes(r.shape)) sawBig = true;
|
||||
if (r.doors.n && !ctx.G.rooms[(r.gx) + ',' + (r.gy - 1)]) reciprocal = false;
|
||||
if (r.doors.s && !ctx.G.rooms[(r.gx) + ',' + (r.gy + 1)]) reciprocal = false;
|
||||
if (r.doors.e && !ctx.G.rooms[(r.gx + 1) + ',' + (r.gy)]) reciprocal = false;
|
||||
if (r.doors.w && !ctx.G.rooms[(r.gx - 1) + ',' + (r.gy)]) reciprocal = false;
|
||||
}
|
||||
}
|
||||
check('floor 6 mixes in big and odd rooms', sawBig);
|
||||
check('big-room maps keep reciprocal doors', reciprocal);
|
||||
ctx.startRun(); step(3, 16);
|
||||
}
|
||||
|
||||
console.log('== XP / perks ==');
|
||||
ctx.gainXP(100);
|
||||
step(5, 16);
|
||||
|
||||
Reference in new issue
Block a user