Implement green teleports and the Lynx/CC2 PRNG
This commit is contained in:
parent
603a74a751
commit
f1b040f176
@ -38,7 +38,9 @@ Give it a try, I guess! [https://c.eev.ee/lexys-labyrinth/](https://c.eev.ee/le
|
||||
|
||||
## Special thanks
|
||||
|
||||
- The incredible nerds who put together the [Chip Wiki](https://wiki.bitbusters.club/) and also reside on the Bit Busters Discord
|
||||
- The incredible nerds who put together the [Chip Wiki](https://wiki.bitbusters.club/) and also reside on the Bit Busters Discord, including:
|
||||
- ruben for documenting the CC2 PRNG
|
||||
- The Architect for documenting the CC2 C2G parser
|
||||
- Everyone who worked on [Chip's Challenge Level Pack 1](https://wiki.bitbusters.club/Chip%27s_Challenge_Level_Pack_1), the default set of levels
|
||||
- [Tile World](https://wiki.bitbusters.club/Tile_World) for being an incredible reference on Lynx mechanics
|
||||
- Everyone who contributed music — see [`js/soundtrack.js`](js/soundtrack.js) for a list!
|
||||
|
||||
69
js/game.js
69
js/game.js
@ -212,6 +212,9 @@ export class Level {
|
||||
this.hint_shown = null;
|
||||
// TODO in lynx/steam, this carries over between levels; in tile world, you can set it manually
|
||||
this.force_floor_direction = 'north';
|
||||
// PRNG is initialized to zero
|
||||
this._rng1 = 0;
|
||||
this._rng2 = 0;
|
||||
|
||||
this.undo_stack = [];
|
||||
this.pending_undo = [];
|
||||
@ -330,6 +333,26 @@ export class Level {
|
||||
}
|
||||
}
|
||||
|
||||
// Lynx PRNG, used unchanged in CC2
|
||||
prng() {
|
||||
// TODO what if we just saved this stuff, as well as the RFF direction, at the beginning of
|
||||
// each tic?
|
||||
let rng1 = this._rng1;
|
||||
let rng2 = this._rng2;
|
||||
this.pending_undo.push(() => {
|
||||
this._rng1 = rng1;
|
||||
this._rng2 = rng2;
|
||||
});
|
||||
|
||||
let n = (this._rng1 >> 2) - this._rng1;
|
||||
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);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Move the game state forwards by one tic
|
||||
advance_tic(p1_primary_direction, p1_secondary_direction) {
|
||||
if (this.state !== 'playing') {
|
||||
@ -906,25 +929,55 @@ export class Level {
|
||||
}
|
||||
|
||||
// Handle teleporting, now that the dust has cleared
|
||||
// FIXME something funny happening here, your input isn't ignore while walking out of it?
|
||||
// FIXME something funny happening here, your input isn't ignored while walking out of it?
|
||||
if (teleporter) {
|
||||
for (let dest of teleporter.type.teleport_dest_order(teleporter, this)) {
|
||||
let original_direction = actor.direction;
|
||||
let success = false;
|
||||
for (let dest of teleporter.type.teleport_dest_order(teleporter, this, actor)) {
|
||||
// Teleporters already containing an actor are blocked and unusable
|
||||
if (dest.cell.some(tile => tile.type.is_actor && tile !== actor))
|
||||
continue;
|
||||
|
||||
// Physically move the actor to the new teleporter
|
||||
// XXX is this right, compare with tile world? i overhear it's actually implemented as a slide?
|
||||
// XXX lynx treats this as a slide and does it in a pass in the main loop
|
||||
// XXX not especially undo-efficient
|
||||
this.remove_tile(actor);
|
||||
this.add_tile(actor, dest.cell);
|
||||
if (this.attempt_step(actor, actor.direction)) {
|
||||
// Success, teleportation complete
|
||||
// Sound plays from the origin cell simply because that's where the sfx player
|
||||
// thinks the player is currently; position isn't updated til next turn
|
||||
this.sfx.play_once('teleport', dest.cell);
|
||||
|
||||
// Red and green teleporters attempt to spit you out in every direction before
|
||||
// giving up on a destination (but not if you return to the original).
|
||||
// Note that we use actor.direction here (rather than original_direction) because
|
||||
// green teleporters modify it in teleport_dest_order, to randomize the exit
|
||||
// direction
|
||||
let direction = actor.direction;
|
||||
let num_directions = 1;
|
||||
if (teleporter.type.teleport_try_all_directions && dest !== teleporter) {
|
||||
num_directions = 4;
|
||||
}
|
||||
for (let i = 0; i < num_directions; i++) {
|
||||
if (this.attempt_step(actor, direction)) {
|
||||
success = true;
|
||||
// Sound plays from the origin cell simply because that's where the sfx player
|
||||
// thinks the player is currently; position isn't updated til next turn
|
||||
this.sfx.play_once('teleport', teleporter.cell);
|
||||
break;
|
||||
}
|
||||
else {
|
||||
direction = DIRECTIONS[direction].right;
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
else if (num_directions === 4) {
|
||||
// Restore our original facing before continuing
|
||||
// (For red teleports, we try every possible destination in our original
|
||||
// movement direction, so this is correct. For green teleports, we only try one
|
||||
// destination and then fall back to walking through the source in our original
|
||||
// movement direction, so this is still correct.)
|
||||
this.set_actor_direction(actor, original_direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -818,29 +818,41 @@ const TILE_TYPES = {
|
||||
},
|
||||
teleport_blue: {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
teleport_dest_order(me, level) {
|
||||
teleport_dest_order(me, level, other) {
|
||||
return level.iter_tiles_in_reading_order(me.cell, 'teleport_blue', true);
|
||||
},
|
||||
},
|
||||
teleport_red: {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
teleport_dest_order(me, level) {
|
||||
// FIXME you can control your exit direction from red teleporters
|
||||
teleport_try_all_directions: true,
|
||||
teleport_allow_override: true,
|
||||
teleport_dest_order(me, level, other) {
|
||||
return level.iter_tiles_in_reading_order(me.cell, 'teleport_red');
|
||||
},
|
||||
},
|
||||
teleport_green: {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
teleport_dest_order(me, level) {
|
||||
// FIXME exit direction is random; unclear if it's any direction or only unblocked ones
|
||||
teleport_try_all_directions: true,
|
||||
teleport_dest_order(me, level, other) {
|
||||
let all = Array.from(level.iter_tiles_in_reading_order(me.cell, 'teleport_green'));
|
||||
// FIXME this should use the lynxish rng
|
||||
return [random_choice(all), me];
|
||||
if (all.length <= 1) {
|
||||
// If this is the only teleporter, just walk out the other side — and, crucially, do
|
||||
// NOT advance the PRNG
|
||||
return [me];
|
||||
}
|
||||
// Note the iterator starts on the /next/ teleporter, so there's an implicit +1 here.
|
||||
// The -1 is to avoid spitting us back out of the same teleporter, which will be last in
|
||||
// the list
|
||||
let target = all[level.prng() % (all.length - 1)];
|
||||
// Also set the actor's (initial) exit direction
|
||||
level.set_actor_direction(other, ['north', 'east', 'south', 'west'][level.prng() % 4]);
|
||||
return [target, me];
|
||||
},
|
||||
},
|
||||
teleport_yellow: {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
teleport_dest_order(me, level) {
|
||||
teleport_allow_override: true,
|
||||
teleport_dest_order(me, level, other) {
|
||||
// FIXME special pickup behavior; NOT an item though, does not combine with no sign
|
||||
return level.iter_tiles_in_reading_order(me.cell, 'teleport_yellow', true);
|
||||
},
|
||||
@ -1517,7 +1529,7 @@ for (let [name, type] of Object.entries(TILE_TYPES)) {
|
||||
|
||||
if (type.draw_layer === undefined ||
|
||||
type.draw_layer !== Math.floor(type.draw_layer) ||
|
||||
type.draw_layer >= 4)
|
||||
type.draw_layer >= 5)
|
||||
{
|
||||
console.error(`Tile type ${name} has a bad draw layer`);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user