From 8d197ce4790681bb7d46df8e0cae317203bb7045 Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Wed, 2 Dec 2020 15:03:13 -0700 Subject: [PATCH] Add a basic implementation of doppelgangers --- js/format-c2g.js | 2 -- js/game.js | 56 +++++++++++++++++++++++++++++++++++++----------- js/tileset.js | 19 +++++++++++++--- js/tiletypes.js | 53 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 17 deletions(-) diff --git a/js/format-c2g.js b/js/format-c2g.js index 998ff1f..f5f0f06 100644 --- a/js/format-c2g.js +++ b/js/format-c2g.js @@ -496,13 +496,11 @@ const TILE_ENCODING = { name: 'doppelganger1', has_next: true, extra_args: [arg_direction], - error: "Doppelganger Lexy is not yet implemented, sorry!", }, 0x66: { name: 'doppelganger2', has_next: true, extra_args: [arg_direction], - error: "Doppelganger Cerise is not yet implemented, sorry!", }, 0x68: { name: 'bowling_ball', diff --git a/js/game.js b/js/game.js index 6e31a9b..eee78a8 100644 --- a/js/game.js +++ b/js/game.js @@ -48,10 +48,10 @@ export class Tile { // the name is the same? blocks(other, direction, level) { // Extremely awkward special case: items don't block monsters if the cell also contains an - // item modifier (i.e. "no" sign) or a player + // item modifier (i.e. "no" sign) or a real player // TODO would love to get this outta here if (this.type.is_item && - this.cell.some(tile => tile.type.item_modifier || tile.type.is_player)) + this.cell.some(tile => tile.type.item_modifier || tile.type.is_real_player)) return false; if (this.type.blocks_collision & other.type.collision_mask) @@ -281,6 +281,7 @@ export class Level { let n = 0; let connectables = []; this.power_sources = []; + this.players = []; // FIXME handle traps correctly: // - if an actor is in the cell, set the trap to open and unstick everything in it for (let y = 0; y < this.height; y++) { @@ -304,10 +305,8 @@ export class Level { this.power_sources.push(tile); } - if (tile.type.is_player) { - // TODO handle multiple players, also chip and melinda both - // TODO complain if no player - this.player = tile; + if (tile.type.is_real_player) { + this.players.push(tile); } if (tile.type.is_actor) { this.actors.push(tile); @@ -320,6 +319,12 @@ export class Level { } } } + // TODO complain if no player + this.player = this.players[0]; + this.player_index = 0; + // Used for doppelgangers + this.player1_move = null; + this.player2_move = null; // Connect buttons and teleporters let num_cells = this.width * this.height; @@ -485,7 +490,8 @@ export class Level { for (let key of [ '_rng1', '_rng2', '_blob_modifier', 'force_floor_direction', 'tic_counter', 'time_remaining', 'timer_paused', - 'chips_remaining', 'bonus_points', 'hint_shown', 'state' + 'chips_remaining', 'bonus_points', 'hint_shown', 'state', + 'player1_move', 'player2_move', ]) { this.pending_undo.level_props[key] = this[key]; } @@ -608,6 +614,7 @@ export class Level { // XXX this in particular has some subtleties in lynx (e.g. you // can override forwards??) and DEFINITELY all kinds of stuff // in ms + // XXX unclear what impact this has on doppelgangers if (actor === this.player && p1_primary_direction && actor.last_move_was_force) @@ -624,6 +631,15 @@ export class Level { continue; } else if (actor === this.player) { + // Sorry for the confusion; "p1" and "p2" in the direction args refer to physical + // human players, NOT to the two types of player tiles! + if (this.player.type.name === 'player') { + this.player1_move = p1_primary_direction; + } + else { + this.player2_move = p1_primary_direction; + } + if (p1_primary_direction) { actor.decision = p1_primary_direction; this._set_tile_prop(actor, 'last_move_was_force', false); @@ -637,6 +653,11 @@ export class Level { // go back into the trap. this is consistent with CC2 but not ms/lynx continue; } + // FIXME seems like all this could be moved into a tile type function since it's all + // only used basically one time each + else if (actor.type.decide_movement) { + direction_preference = actor.type.decide_movement(actor, this); + } else if (actor.type.movement_mode === 'forward') { // blue tank behavior: keep moving forward, reverse if the flag is set let direction = actor.direction; @@ -791,7 +812,7 @@ export class Level { // Players can also bump the tiles in the cell next to the one they're leaving let dir2 = actor.secondary_direction; - if (actor.type.is_player && dir2 && + if (actor.type.is_real_player && dir2 && ! old_cell.blocks_leaving(actor, dir2)) { let neighbor = this.get_neighboring_cell(old_cell, dir2); @@ -811,6 +832,18 @@ export class Level { } } + // In the event that the player is sliding (and thus not deliberately moving), remember + // their current movement direction. + // This is hokey, and doing it here is even hokier, but it seems to match CC2 behavior. + if (this.player.movement_cooldown > 0) { + if (this.player.type.name === 'player') { + this.player1_move = this.player.direction; + } + else { + this.player2_move = this.player.direction; + } + } + // Strip out any destroyed actors from the acting order // FIXME this is O(n), where n is /usually/ small, but i still don't love it let p = 0; @@ -999,18 +1032,17 @@ export class Level { } // Check for a couple effects that always apply immediately - // TODO do blocks smash monsters? if (actor === this.player) { this.hint_shown = null; } for (let tile of goal_cell) { - if (actor.type.is_player && tile.type.is_monster) { + if (actor.type.is_real_player && tile.type.is_monster) { this.fail(tile.type.name); } - else if (actor.type.is_monster && tile.type.is_player) { + else if (actor.type.is_monster && tile.type.is_real_player) { this.fail(actor.type.name); } - else if (actor.type.is_block && tile.type.is_player) { + else if (actor.type.is_block && tile.type.is_real_player) { this.fail('squished'); } diff --git a/js/tileset.js b/js/tileset.js index 5ad9229..66a9e6a 100644 --- a/js/tileset.js +++ b/js/tileset.js @@ -142,7 +142,15 @@ export const CC2_TILESET_LAYOUT = { hiking_boots: [4, 6], lightning_bolt: [5, 6], // weird translucent spiral - // weird translucent red + // TODO dopps can push but i don't think they have any other visuals + doppelganger1: { + base: [7, 6], + overlay: 'player', + }, + doppelganger2: { + base: [7, 6], + overlay: 'player2', + }, button_blue: [8, 6], button_green: [9, 6], button_red: [10, 6], @@ -946,10 +954,15 @@ export class Tileset { if (drawspec.overlay) { // Goofy overlay thing used for green/purple toggle tiles and - // southeast thin walls. Draw the base (a type name), then draw + // southeast thin walls. Draw the base (a type name or drawspec), then draw // the overlay (either a type name or a regular draw spec). // TODO chance of infinite recursion here - this.draw_type(drawspec.base, tile, tic, blit); + if (typeof drawspec.base === 'string') { + this.draw_type(drawspec.base, tile, tic, blit); + } + else { + this._draw_standard(drawspec.base, tile, tic, blit); + } if (typeof drawspec.overlay === 'string') { this.draw_type(drawspec.overlay, tile, tic, blit); return; diff --git a/js/tiletypes.js b/js/tiletypes.js index b2378d2..273894f 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -1716,6 +1716,7 @@ const TILE_TYPES = { draw_layer: DRAW_LAYERS.actor, is_actor: true, is_player: true, + is_real_player: true, collision_mask: COLLISION.player1, has_inventory: true, can_reveal_walls: true, @@ -1734,6 +1735,7 @@ const TILE_TYPES = { draw_layer: DRAW_LAYERS.actor, is_actor: true, is_player: true, + is_real_player: true, collision_mask: COLLISION.player2, has_inventory: true, can_reveal_walls: true, @@ -1749,6 +1751,57 @@ const TILE_TYPES = { }, visual_state: player_visual_state, }, + doppelganger1: { + draw_layer: DRAW_LAYERS.actor, + is_actor: true, + is_player: true, + is_monster: true, + collision_mask: COLLISION.player1, + blocks_collision: COLLISION.all_but_player, + has_inventory: true, + can_reveal_walls: true, // XXX i think? + movement_speed: 4, + movement_mode: 'copy1', + pushes: { + dirt_block: true, + ice_block: true, + directional_block: true, + }, + infinite_items: { + key_green: true, + }, + decide_movement(me, level) { + if (level.player1_move) { + return [level.player1_move]; + } + else { + return null; + } + }, + //visual_state: doppelganger_visual_state, + }, + doppelganger2: { + draw_layer: DRAW_LAYERS.actor, + is_actor: true, + is_player: true, + is_monster: true, + collision_mask: COLLISION.player2, + blocks_collision: COLLISION.all_but_player, + has_inventory: true, + can_reveal_walls: true, // XXX i think? + movement_speed: 4, + movement_mode: 'copy2', + ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se']), + pushes: { + dirt_block: true, + ice_block: true, + directional_block: true, + }, + infinite_items: { + key_yellow: true, + }, + //visual_state: doppelganger_visual_state, + }, chip: { draw_layer: DRAW_LAYERS.item, is_chip: true,