From 0c9a7e3d0765af7db608de9e36bc1ebdb1a058a3 Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Sat, 24 Oct 2020 20:49:14 -0600 Subject: [PATCH] Implement all three blob modes and fix up some minor details This makes the replays from the Steam copies of Blobnet and Nice Day play back correctly! Neato! --- js/format-base.js | 4 ++++ js/format-c2g.js | 2 +- js/game.js | 61 +++++++++++++++++++++++++++++++++++++++-------- js/main.js | 6 +++-- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/js/format-base.js b/js/format-base.js index f9cf4d0..0c43eb5 100644 --- a/js/format-base.js +++ b/js/format-base.js @@ -16,6 +16,10 @@ export class StoredLevel { this.extra_chunks = []; this.use_cc1_boots = false; this.use_ccl_compat = false; + // 0 - deterministic (PRNG + simple convolution) + // 1 - 4 patterns (default; PRNG + rotating through 0-3) + // 2 - extra random (like deterministic, but initial seed is "actually" random) + this.blob_behavior = 1; this.size_x = 0; this.size_y = 0; diff --git a/js/format-c2g.js b/js/format-c2g.js index 2207ea8..7cb5ecb 100644 --- a/js/format-c2g.js +++ b/js/format-c2g.js @@ -835,7 +835,7 @@ export function parse_level(buf, number = 1) { if (view.byteLength <= 24) continue; - //level.blob_behavior = view.getUint8(24, true); + level.blob_behavior = view.getUint8(24, true); } else if (type === 'MAP ' || type === 'PACK') { if (type === 'PACK') { diff --git a/js/game.js b/js/game.js index 69c17e4..ab60040 100644 --- a/js/game.js +++ b/js/game.js @@ -215,6 +215,13 @@ export class Level { // PRNG is initialized to zero this._rng1 = 0; this._rng2 = 0; + if (this.stored_level.blob_behavior === 0) { + this._blob_modifier = 0x55; + } + else { + // The other two modes are initialized to a random seed + this._blob_modifier = Math.floor(Math.random() * 256); + } this.undo_stack = []; this.pending_undo = []; @@ -348,11 +355,34 @@ export class Level { if (!(this._rng1 & 0x02)) --n; this._rng1 = (this._rng1 >> 1) | (this._rng2 & 0x80); this._rng2 = (this._rng2 << 1) | (n & 0x01); - let ret = (this._rng1 ^ this._rng2) & 0xFF; - console.log(ret); + let ret = (this._rng1 ^ this._rng2) & 0xff; return ret; } + // Weird thing done by CC2 to make blobs... more... random + get_blob_modifier() { + let mod = this._blob_modifier; + this.pending_undo.push(() => this._blob_modifier = mod); + + if (this.stored_level.blob_behavior === 1) { + // "4 patterns" just increments by 1 every time (but /after/ returning) + //this._blob_modifier = (this._blob_modifier + 1) % 4; + mod = (mod + 1) % 4; + this._blob_modifier = mod; + } + else { + // Other modes do this curious operation + mod *= 2; + if (mod < 255) { + mod ^= 0x1d; + } + mod &= 0xff; + this._blob_modifier = mod; + } + + return mod; + } + // Move the game state forwards by one tic advance_tic(p1_primary_direction, p1_secondary_direction) { if (this.state !== 'playing') { @@ -554,11 +584,9 @@ export class Level { direction_preference = [actor.direction, d.opposite]; } else if (actor.type.movement_mode === 'bounce-random') { - // walker behavior: preserve current direction; if that - // doesn't work, pick a random direction, even the one we - // failed to move in - // TODO unclear if this is right in cc2 as well. definitely not in ms, which chooses a legal move - direction_preference = [actor.direction, ['north', 'south', 'east', 'west'][Math.floor(Math.random() * 4)]]; + // walker behavior: preserve current direction; if that doesn't work, pick a random + // direction, even the one we failed to move in (but ONLY then) + direction_preference = [actor.direction, 'WALKER']; } else if (actor.type.movement_mode === 'pursue') { // teeth behavior: always move towards the player @@ -597,14 +625,27 @@ export class Level { } else if (actor.type.movement_mode === 'random') { // blob behavior: move completely at random - // TODO cc2 has twiddles for how this works per-level, as well as the initial seed for demo playback - direction_preference = [['north', 'south', 'east', 'west'][Math.floor(Math.random() * 4)]]; + let modifier = this.get_blob_modifier(); + direction_preference = [['north', 'east', 'south', 'west'][(this.prng() + modifier) % 4]]; } // Check which of those directions we *can*, probably, move in // TODO i think player on force floor will still have some issues here + // FIXME probably bail earlier for stuck actors so the prng isn't advanced? what is the + // lynx behavior? also i hear something about blobs on cloners?? if (direction_preference && ! actor.stuck) { + let fallback_direction; for (let direction of direction_preference) { + if (direction === 'WALKER') { + // Walkers roll a random direction ONLY if their first attempt was blocked + direction = actor.direction; + let num_turns = this.prng() % 4; + for (let i = 0; i < num_turns; i++) { + direction = DIRECTIONS[direction].right; + } + } + fallback_direction = direction; + let dest_cell = this.get_neighboring_cell(actor.cell, direction); if (! dest_cell) continue; @@ -621,7 +662,7 @@ export class Level { // If all the decisions are blocked, actors still try the last one (and might even // be able to move that way by the time their turn comes around!) if (actor.decision === null) { - actor.decision = direction_preference[direction_preference.length - 1]; + actor.decision = fallback_direction; } } } diff --git a/js/main.js b/js/main.js index 13f5d4e..a4642ff 100644 --- a/js/main.js +++ b/js/main.js @@ -666,8 +666,10 @@ class Player extends PrimaryView { play_demo() { this.restart_level(); - this.demo_faucet = this.level.stored_level.demo[Symbol.iterator](); - this.level.force_floor_direction = this.level.stored_level.demo.initial_force_floor_direction; + let demo = this.level.stored_level.demo; + this.demo_faucet = demo[Symbol.iterator](); + this.level.force_floor_direction = demo.initial_force_floor_direction; + this.level._blob_modifier = demo.blob_seed; // FIXME should probably start playback on first real input this.set_state('playing'); }