From 2381bd38b9533c93f4cf2f186e986bbdcee5e04c Mon Sep 17 00:00:00 2001 From: "Eevee (Evelyn Woods)" Date: Wed, 23 Dec 2020 04:30:10 -0700 Subject: [PATCH] Add compat switches for using the CC2 timing and update order Other gameplay changes/fixes that crept in: - Ghosts no longer pick up red keys - Doppelgangers now read their movement directly from players, so no intermediate variables are necessary - Spring mining is no longer possible - Push recursion is detected and prevented - Bowling balls will also blow up anything that runs into them --- js/game.js | 412 +++++++++++++++++++++++++++++------------------- js/main.js | 13 +- js/tiletypes.js | 45 ++++-- 3 files changed, 300 insertions(+), 170 deletions(-) diff --git a/js/game.js b/js/game.js index 3b63383..2f19ccb 100644 --- a/js/game.js +++ b/js/game.js @@ -259,30 +259,45 @@ export class Cell extends Array { // If we got this far, all that's left is to deal with pushables if (pushable_tiles.length > 0) { - let neighbor_cell = level.get_neighboring_cell(this, direction); - if (! neighbor_cell) - return false; - - for (let tile of pushable_tiles) { - if (push_mode === 'bump') { - // FIXME and leaving! - if (! neighbor_cell.try_entering(tile, direction, level, push_mode)) + // This ends recursive push attempts, which can happen with a row of ice clogged by ice + // blocks that are trying to slide + actor._trying_to_push = true; + try { + for (let tile of pushable_tiles) { + if (tile._trying_to_push) return false; - } - else if (push_mode === 'push') { - if (actor === level.player) { - level._set_tile_prop(actor, 'is_pushing', true); + if (push_mode === 'bump') { + // FIXME this doesn't take railroad curves into account, e.g. it thinks a + // rover can't push a block through a curve + if (! level.check_movement(tile, tile.cell, direction, push_mode)) + return false; } - if (! level.attempt_out_of_turn_step(tile, direction)) { - if (tile.slide_mode !== null && tile.movement_cooldown !== 0) { - // If the push failed and the obstacle is in the middle of a slide, - // remember this as the next move it'll make - level._set_tile_prop(tile, 'pending_push', direction); + else if (push_mode === 'push') { + if (actor === level.player) { + level._set_tile_prop(actor, 'is_pushing', true); + } + if (! level.attempt_out_of_turn_step(tile, direction)) { + if (tile.slide_mode !== null && tile.movement_cooldown !== 0) { + // If the push failed and the obstacle is in the middle of a slide, + // remember this as the next move it'll make + level._set_tile_prop(tile, 'pending_push', direction); + } + return false; } - return false; } } } + finally { + delete actor._trying_to_push; + } + + // In push mode, check one last time for being blocked, in case we e.g. pushed a block + // off of a recessed wall + // TODO deleting this allows spring mining, though i ended up causing it in a more + // aggressive form; try deleting this and running the 163 BLOX replay, it happens with + // ice blocks near the end + if (push_mode === 'push' && this.some(tile => tile.blocks(actor, direction, level))) + return false; } return true; @@ -420,9 +435,6 @@ export class Level extends LevelInterface { } } // TODO complain if no player - // Used for doppelgangers - this.player1_move = null; - this.player2_move = null; // Connect buttons and teleporters let num_cells = this.width * this.height; @@ -657,9 +669,10 @@ export class Level extends LevelInterface { } this.begin_tic(p1_input); - this.finish_tic(p1_input); + this.finish_tic(); } + // FIXME a whole bunch of these comments are gonna be wrong or confusing now begin_tic(p1_input) { if (this.undo_enabled) { // Store some current level state in the undo entry. (These will often not be modified, but @@ -668,7 +681,7 @@ export class Level extends LevelInterface { '_rng1', '_rng2', '_blob_modifier', 'force_floor_direction', 'tic_counter', 'time_remaining', 'timer_paused', 'chips_remaining', 'bonus_points', 'hint_shown', 'state', - 'player1_move', 'player2_move', 'remaining_players', 'player', + 'remaining_players', 'player', ]) { this.pending_undo.level_props[key] = this[key]; } @@ -677,11 +690,49 @@ export class Level extends LevelInterface { this.p1_released |= ~p1_input; // Action keys released since we last checked them this.swap_player1 = false; - // This effect only lasts one tic, after which we can move again - this._set_tile_prop(this.player, 'is_blocked', false); - this.sfx.set_player_position(this.player.cell); + if (this.compat.use_lynx_loop) { + if (this.compat.emulate_60fps) { + this._begin_tic_lynx60(); + } + else { + this._begin_tic_lynx(); + } + } + else { + this._begin_tic_lexy(); + } + } + + // Only the Lexy-style loop has a notion of "finishing" a tic, since (unlike the Lynx loop) the + // decision phase happens in the /middle/ + finish_tic() { + if (this.compat.use_lynx_loop) { + return; + } + + this._do_decision_phase(); + + // Lexy's separate movement loop + for (let i = this.actors.length - 1; i >= 0; i--) { + let actor = this.actors[i]; + if (! actor.cell) + continue; + + this._do_actor_movement(actor, actor.decision); + } + + this._do_cleanup_phase(); + } + + // Lexy-style loop, the one I stumbled upon accidentally. Here, cooldowns happen /first/ as + // their own phase, then decisions are made, then movement happens. This approach has several + // advantages: there's no cooldown on the same tic that movement begins, which lets the renderer + // interpolate between tics more easily; there's no jitter when pushing a block, as is seen in + // CC2; and generally more things happen in parallel, which improves the illusion that all the + // game objects are acting simultaneously. + _begin_tic_lexy() { // CC2 wiring runs every frame, not every tic, so we need to do it three times, but dealing // with it is delicate. We want the result of a button press to draw, but not last longer // than intended, so we only want one update between the end of the cooldown pass and the @@ -693,12 +744,8 @@ export class Level extends LevelInterface { } this.update_wiring(); - // FIRST PASS: actors tick their cooldowns, finish their movement, and possibly step on - // cells they were moving into. This has a few advantages: it makes rendering interpolation - // much easier, and doing it as a separate pass from /starting/ movement (unlike Lynx) - // improves the illusion that everything is happening simultaneously. - // Note that, as far as I can tell, CC2 actually runs this pass every /frame/. We do not! - // Also Note that we iterate in reverse order, DESPITE keeping dead actors around with null + // Advance everyone's cooldowns + // Note that we iterate in reverse order, DESPITE keeping dead actors around with null // cells, to match the Lynx and CC2 behavior. This is actually important in some cases; // check out the start of CCLP3 #54, where the gliders will eat the blue key immediately if // they act in forward order! (More subtly, even the decision pass does things like @@ -709,44 +756,13 @@ export class Level extends LevelInterface { if (! actor.cell) continue; - if (actor.movement_cooldown <= 0) - continue; - - if (actor.cooldown_delay_hack) { - // See the extensive comment in attempt_out_of_turn_step - actor.cooldown_delay_hack += 1; - continue; - } - - this._set_tile_prop(actor, 'movement_cooldown', Math.max(0, actor.movement_cooldown - 1)); - - if (actor.movement_cooldown <= 0) { - if (actor.type.ttl) { - // This is an animation that just finished, so destroy it - this.remove_tile(actor); - continue; - } - - if (! this.compat.tiles_react_instantly) { - this.step_on_cell(actor, actor.cell); - } - // Erase any trace of being in mid-movement, however: - // - This has to happen after stepping on cells, because some effects care about - // the cell we're arriving from - // - Don't do it if stepping on the cell caused us to move again - if (actor.movement_cooldown <= 0) { - this._set_tile_prop(actor, 'previous_cell', null); - this._set_tile_prop(actor, 'movement_speed', null); - if (actor.is_pulled) { - this._set_tile_prop(actor, 'is_pulled', false); - } - } - } + this._do_actor_cooldown(actor, 3); } // Mini extra pass: deal with teleporting separately. Otherwise, actors may have been in // the way of the teleporter but finished moving away during the above loop; this is - // particularly bad when it happens with a block you're pushing. + // particularly bad when it happens with a block you're pushing. (CC2 doesn't need to do + // this because blocks you're pushing are always a frame ahead of you anyway.) for (let i = this.actors.length - 1; i >= 0; i--) { let actor = this.actors[i]; if (! actor.cell) @@ -757,22 +773,38 @@ export class Level extends LevelInterface { } } - // Here's the third. this.update_wiring(); } - finish_tic() { - // After cooldowns but before the decision phase, remember the player's /current/ direction, - // which may be affected by sliding. This will affect the behavior of doppelgangers earlier - // in the actor order than the player. - if (this.player.movement_cooldown > 0) { - this.remember_player_move(this.player.direction); - } - else { - this.remember_player_move(this.player.decision); - } + // Lynx-style loop: everyone decides, then everyone moves/cools. + _begin_tic_lynx() { + this._do_decision_phase(); + this._do_combined_action_phase(3); + this.update_wiring(); - // SECOND PASS: actors decide their upcoming movement simultaneously + this._do_cleanup_phase(); + } + + // Same as above, but split up to run at 60fps, where only every third frame allows for + // decisions. This is how CC2 works. + _begin_tic_lynx60() { + this._do_decision_phase(true); + this._do_combined_action_phase(1, true); + this.update_wiring(); + + this._do_decision_phase(true); + this._do_combined_action_phase(1, true); + this.update_wiring(); + + this._do_decision_phase(); + this._do_combined_action_phase(1); + this.update_wiring(); + + this._do_cleanup_phase(); + } + + // Decision phase: all actors decide on their movement "simultaneously" + _do_decision_phase(forced_only = false) { for (let i = this.actors.length - 1; i >= 0; i--) { let actor = this.actors[i]; @@ -786,62 +818,117 @@ export class Level extends LevelInterface { if (actor.movement_cooldown > 0) continue; + if (! forced_only && actor.type.on_tic) { + actor.type.on_tic(actor, this); + if (! actor.cell) + continue; + } + if (actor === this.player) { - this.make_player_decision(actor, this.p1_input); + this.make_player_decision(actor, this.p1_input, forced_only); } else { - this.make_actor_decision(actor); + this.make_actor_decision(actor, forced_only); } } // This only persists for a single decision phase - this.yellow_tank_decision = null; + if (! forced_only) { + this.yellow_tank_decision = null; + } + } - // THIRD PASS: everyone actually moves + // Lynx's combined action phase: each actor attempts to move, then cools down, in order + _do_combined_action_phase(cooldown, forced_only = false) { for (let i = this.actors.length - 1; i >= 0; i--) { let actor = this.actors[i]; if (! actor.cell) continue; - if (actor.type.on_tic) { - actor.type.on_tic(actor, this); + this._do_actor_movement(actor, actor.decision); + this._do_actor_cooldown(actor, cooldown); + if (actor.just_stepped_on_teleporter) { + this.attempt_teleport(actor); } + } + } - // Check this again, since an earlier pass may have caused us to start moving - if (actor.movement_cooldown > 0) - continue; + // Have an actor attempt to move + _do_actor_movement(actor, direction) { + // Check this again, since an earlier pass may have caused us to start moving + if (actor.movement_cooldown > 0) + return; - if (! actor.decision) - continue; + if (! direction) + return true; - // Actor is allowed to move, so do so - let success = this.attempt_step(actor, actor.decision); + // Actor is allowed to move, so do so + let success = this.attempt_step(actor, direction); - // FIXME not convinced that ice bonking should actually go here. in cc2 it appears to - // happen every frame, fwiw, but i'm not sure if that includes frames with forced moves - // (though i guess that's impossible) - if (! success) { - let terrain = actor.cell.get_terrain(); - if (terrain.type.slide_mode === 'ice' && (! actor.ignores(terrain.type.name) || - // TODO weird cc2 quirk/bug: ghosts bonk on ice even though they don't slide on it - // FIXME and if they have cleats, they get stuck instead (?!) - (actor.type.name === 'ghost' && actor.cell.get_terrain().type.slide_mode === 'ice'))) - { - // Bonk on ice: turn the actor around, consult the tile in case it's an ice - // corner, and try again - actor.direction = DIRECTIONS[actor.decision].opposite; - actor.decision = terrain.type.get_slide_direction(terrain, this, actor); - success = this.attempt_step(actor, actor.decision); - } - } - - // Track whether the player is blocked, for visual effect - if (actor === this.player && actor.decision && ! success) { - this.sfx.play_once('blocked'); - this._set_tile_prop(actor, 'is_blocked', true); + // FIXME not convinced that ice bonking should actually go here. in cc2 it appears to + // happen every frame, fwiw, but i'm not sure if that includes frames with forced moves + // (though i guess that's impossible) + if (! success) { + let terrain = actor.cell.get_terrain(); + if (terrain.type.slide_mode === 'ice' && (! actor.ignores(terrain.type.name) || + // TODO weird cc2 quirk/bug: ghosts bonk on ice even though they don't slide on it + // FIXME and if they have cleats, they get stuck instead (?!) + (actor.type.name === 'ghost' && actor.cell.get_terrain().type.slide_mode === 'ice'))) + { + // Bonk on ice: turn the actor around, consult the tile in case it's an ice + // corner, and try again + actor.direction = DIRECTIONS[direction].opposite; + direction = terrain.type.get_slide_direction(terrain, this, actor); + success = this.attempt_step(actor, direction); } } + // Track whether the player is blocked, both for visual effect and for doppelgangers + if (actor === this.player && ! success) { + this.sfx.play_once('blocked'); + this._set_tile_prop(actor, 'is_blocked', true); + } + + return success; + } + + _do_actor_cooldown(actor, cooldown = 3) { + if (actor.movement_cooldown <= 0) + return; + + if (actor.cooldown_delay_hack) { + // See the extensive comment in attempt_out_of_turn_step + actor.cooldown_delay_hack += 1; + return; + } + + this._set_tile_prop(actor, 'movement_cooldown', Math.max(0, actor.movement_cooldown - cooldown)); + + if (actor.movement_cooldown <= 0) { + if (actor.type.ttl) { + // This is an animation that just finished, so destroy it + this.remove_tile(actor); + return; + } + + if (! this.compat.tiles_react_instantly) { + this.step_on_cell(actor, actor.cell); + } + // Erase any trace of being in mid-movement, however: + // - This has to happen after stepping on cells, because some effects care about + // the cell we're arriving from + // - Don't do it if stepping on the cell caused us to move again + if (actor.movement_cooldown <= 0) { + this._set_tile_prop(actor, 'previous_cell', null); + this._set_tile_prop(actor, 'movement_speed', null); + if (actor.is_pulled) { + this._set_tile_prop(actor, 'is_pulled', false); + } + } + } + } + + _do_cleanup_phase() { // 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. not strictly // necessary, either; maybe only do it every few tics? @@ -902,7 +989,8 @@ export class Level extends LevelInterface { // Advance the clock // TODO i suspect cc2 does this at the beginning of the tic, but even if you've won? if you - // step on a penalty + exit you win, but you see the clock flicker 1 for a single frame + // step on a penalty + exit you win, but you see the clock flicker 1 for a single frame. + // maybe the win check happens at the start of the frame too? this.tic_counter += 1; if (this.time_remaining !== null && ! this.timer_paused) { this.time_remaining -= 1; @@ -942,9 +1030,15 @@ export class Level extends LevelInterface { return [dir1, dir2]; } - make_player_decision(actor, input) { + make_player_decision(actor, input, forced_only = false) { // Only reset the player's is_pushing between movement, so it lasts for the whole push this._set_tile_prop(actor, 'is_pushing', false); + // This effect only lasts one tic, after which we can move again. Note that this one has + // gameplay impact -- doppelgangers use it to know if they should copy your facing direction + // even if you're not moving + if (! forced_only) { + this._set_tile_prop(actor, 'is_blocked', false); + } // If the game has already been won (or lost), don't bother with a move; it'll misalign the // player from their actual position and not accomplish anything gameplay-wise. @@ -978,7 +1072,7 @@ export class Level extends LevelInterface { if (actor.slide_mode) { forced_decision = terrain.type.get_slide_direction(terrain, this, actor); } - let may_move = (! actor.slide_mode || (actor.slide_mode === 'force' && actor.last_move_was_force)); + let may_move = ! forced_only && (! actor.slide_mode || (actor.slide_mode === 'force' && actor.last_move_was_force)); let [dir1, dir2] = this._extract_player_directions(input); // Check for special player actions, which can only happen at decision time. Dropping can @@ -986,21 +1080,23 @@ export class Level extends LevelInterface { // can be done freely while sliding. // FIXME cc2 seems to rely on key repeat for this; if you have four bowling balls and hold // Q, you'll throw the first, wait a second or so, then release the rest rapid-fire. absurd - let new_input = input & this.p1_released; - this.p1_released = 0xff; - if (new_input & INPUT_BITS.cycle) { - this.cycle_inventory(this.player); - this.p1_released &= ~INPUT_BITS.cycle; - } - if ((new_input & INPUT_BITS.drop) && may_move) { - this.drop_item(this.player); - this.p1_released &= ~INPUT_BITS.drop; - } - if ((new_input & INPUT_BITS.swap) && this.remaining_players > 1) { - // This is delayed until the end of the tic to avoid screwing up anything - // checking this.player - this.swap_player1 = true; - this.p1_released &= ~INPUT_BITS.swap; + if (! forced_only) { + let new_input = input & this.p1_released; + this.p1_released = 0xff; + if (new_input & INPUT_BITS.cycle) { + this.cycle_inventory(this.player); + this.p1_released &= ~INPUT_BITS.cycle; + } + if ((new_input & INPUT_BITS.drop) && may_move) { + this.drop_item(this.player); + this.p1_released &= ~INPUT_BITS.drop; + } + if ((new_input & INPUT_BITS.swap) && this.remaining_players > 1) { + // This is delayed until the end of the tic to avoid screwing up anything + // checking this.player + this.swap_player1 = true; + this.p1_released &= ~INPUT_BITS.swap; + } } if (actor.slide_mode && ! (may_move && dir1)) { @@ -1011,7 +1107,7 @@ export class Level extends LevelInterface { this._set_tile_prop(actor, 'last_move_was_force', true); } } - else if (dir1 === null) { + else if (dir1 === null || forced_only) { // Not attempting to move, so do nothing } else { @@ -1080,14 +1176,9 @@ export class Level extends LevelInterface { this._set_tile_prop(actor, 'last_move_was_force', false); } } - - // Remember our choice for the sake of doppelgangers - // FIXME still a bit unclear on how they handle secondary direction, but i'm not sure that's - // even a real concept in lynx, so maybe this is right?? - this.remember_player_move(actor.decision); } - make_actor_decision(actor) { + make_actor_decision(actor, forced_only = false) { // Compat flag for blue tanks if (this.compat.sliding_tanks_ignore_button && actor.slide_mode && actor.pending_reverse) @@ -1114,6 +1205,8 @@ export class Level extends LevelInterface { actor.decision = terrain.type.get_slide_direction(terrain, this, actor); return; } + if (forced_only) + return; if (actor.cell.some(tile => tile.type.traps && tile.type.traps(tile, actor))) { // An actor in a cloner or a closed trap can't turn // TODO because of this, if a tank is trapped when a blue button is pressed, then @@ -1130,9 +1223,9 @@ export class Level extends LevelInterface { return; let all_blocked = true; for (let [i, direction] of direction_preference.entries()) { - if (direction === null) { + if (! direction) { // This actor is giving up! Alas. - actor.decision = direction; + actor.decision = null; break; } if (typeof direction === 'function') { @@ -1224,14 +1317,20 @@ export class Level extends LevelInterface { } this._set_tile_prop(actor, 'previous_cell', actor.cell); - this._set_tile_prop(actor, 'movement_cooldown', speed); - this._set_tile_prop(actor, 'movement_speed', speed); + this._set_tile_prop(actor, 'movement_cooldown', speed * 3); + this._set_tile_prop(actor, 'movement_speed', speed * 3); this.move_to(actor, goal_cell, speed); return true; } attempt_out_of_turn_step(actor, direction) { + if (this.compat.use_lynx_loop) { + let success = this._do_actor_movement(actor, direction); + this._do_actor_cooldown(actor, this.compat.emulate_60fps ? 1 : 3); + return success; + } + if (this.attempt_step(actor, direction)) { // Here's the problem. // In CC2, cooldown is measured in frames, not tics, and it decrements every frame, not @@ -1373,7 +1472,8 @@ export class Level extends LevelInterface { continue; if (tile.type.is_item && - (actor.type.has_inventory || + // FIXME implement item priority i'm begging you + ((actor.type.has_inventory && ! (tile.type.name === 'key_red' && ! actor.type.is_player)) || cell.some(t => t.type.item_modifier === 'pickup')) && this.attempt_take(actor, tile)) { @@ -1471,7 +1571,13 @@ export class Level extends LevelInterface { // Now physically move the actor and have them take a turn this.remove_tile(actor); this.add_tile(actor, dest.cell); + // FIXME i think the cc2 approach might be to handle this at decision time, hence why + // overriding works at all; it happens to work for me because this happens immediately + // before decision time as a separate pass! for now, simulate by undoing the cooldown this.attempt_out_of_turn_step(actor, direction); + if (this.compat.use_lynx_loop && actor.movement_cooldown) { + this._set_tile_prop(actor, 'movement_cooldown', actor.movement_cooldown + 1); + } } if (! success && actor.type.has_inventory && teleporter.type.name === 'teleport_yellow') { // Super duper special yellow teleporter behavior: you pick it the fuck up @@ -1483,17 +1589,6 @@ export class Level extends LevelInterface { } } - remember_player_move(direction) { - if (this.player.type.name === 'player') { - this.player1_move = direction; - this.player2_move = null; - } - else { - this.player1_move = null; - this.player2_move = direction; - } - } - cycle_inventory(actor) { if (this.stored_level.use_cc1_boots) return; @@ -1966,11 +2061,10 @@ export class Level extends LevelInterface { spawn_animation(cell, name) { let type = TILE_TYPES[name]; let tile = new Tile(type); - // Co-opt movement_cooldown/speed for these despite that they aren't moving, since they're - // also used to animate everything else. Decrement the cooldown immediately, to match the - // normal actor behavior of decrementing one's own cooldown at the end of one's turn - // FIXME this replicates cc2 behavior, but it also means the animation is actually visible - // for one less tic than expected + // Co-opt movement_cooldown/speed for these despite that they aren't moving, since those + // properties are also used to animate everything else anyway. Decrement the cooldown + // immediately, as Lynx does; note that Lynx also ticks /and destroys/ animations early in + // the decision phase, but this seems to work out just as well this._set_tile_prop(tile, 'movement_speed', tile.type.ttl); this._set_tile_prop(tile, 'movement_cooldown', tile.type.ttl - 1); cell._add(tile); diff --git a/js/main.js b/js/main.js index f7b4cf5..ff3ba5c 100644 --- a/js/main.js +++ b/js/main.js @@ -1006,6 +1006,12 @@ class Player extends PrimaryView { ret[key] = value; } } + + // XXX do not commit this + /* + ret.use_lynx_loop = true; + ret.emulate_60fps = true; + */ return ret; } @@ -2171,7 +2177,12 @@ class PackTestDialog extends DialogOverlay { // TODO compat options here?? let replay = stored_level.replay; - level = new Level(stored_level, {}); + level = new Level(stored_level, { + /* + use_lynx_loop: true, + emulate_60fps: true, + */ + }); level.sfx = dummy_sfx; level.force_floor_direction = replay.initial_force_floor_direction; level._blob_modifier = replay.blob_seed; diff --git a/js/tiletypes.js b/js/tiletypes.js index a588d2e..3c62d0f 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -2242,7 +2242,22 @@ const TILE_TYPES = { decide_movement(me, level) { return [me.direction]; }, + on_bumped(me, level, other) { + // Blow up anything that runs into us... unless we're on a cloner + // FIXME there are other cases where this won't be right; this shouldn't happen if the + // cell blocks the actor, but i don't have a callback for that? + if (me.cell.has('cloner')) + return; + if (other.type.is_real_player) { + level.fail(me.type.name); + } + else { + level.transmute_tile(other, 'explosion'); + } + level.transmute_tile(me, 'explosion'); + }, on_blocked(me, level, direction) { + // Blow up anything we run into let cell = level.get_neighboring_cell(me.cell, direction); let other; if (cell) { @@ -2392,8 +2407,13 @@ const TILE_TYPES = { key_green: true, }, decide_movement(me, level) { - if (level.player1_move) { - return [level.player1_move]; + if (level.player.type.name === 'player') { + if (level.player.movement_cooldown || level.player.is_blocked) { + return [level.player.direction]; + } + else { + return [level.player.decision]; + } } else { return null; @@ -2427,8 +2447,13 @@ const TILE_TYPES = { key_yellow: true, }, decide_movement(me, level) { - if (level.player2_move) { - return [level.player2_move]; + if (level.player.type.name === 'player2') { + if (level.player.movement_cooldown || level.player.is_blocked) { + return [level.player.direction]; + } + else { + return [level.player.decision]; + } } else { return null; @@ -2550,7 +2575,7 @@ const TILE_TYPES = { is_actor: true, collision_mask: 0, blocks_collision: COLLISION.player, - ttl: 6, + ttl: 16, // If anything else even begins to step on an animation, it's erased // FIXME possibly erased too fast; cc2 shows it briefly? could i get away with on_arrive here? on_approach(me, level, other) { @@ -2562,7 +2587,7 @@ const TILE_TYPES = { is_actor: true, collision_mask: 0, blocks_collision: COLLISION.player, - ttl: 6, + ttl: 16, on_approach(me, level, other) { level.remove_tile(me); }, @@ -2574,7 +2599,7 @@ const TILE_TYPES = { collision_mask: 0, blocks_collision: 0, // determined experimentally - ttl: 12, + ttl: 36, }, // Custom VFX (identical function, but different aesthetic) splash_slime: { @@ -2582,7 +2607,7 @@ const TILE_TYPES = { is_actor: true, collision_mask: 0, blocks_collision: COLLISION.player, - ttl: 6, + ttl: 16, on_approach(me, level, other) { level.remove_tile(me); }, @@ -2593,13 +2618,13 @@ const TILE_TYPES = { draw_layer: DRAW_LAYERS.actor, is_actor: true, collision_mask: 0, - ttl: 8, + ttl: 8 * 3, }, player2_exit: { draw_layer: DRAW_LAYERS.actor, is_actor: true, collision_mask: 0, - ttl: 8, + ttl: 8 * 3, }, // Invalid tiles that appear in some CCL levels because community level