Compare commits
1 Commits
master
...
frame-base
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86764612d3 |
508
js/game.js
508
js/game.js
@ -557,23 +557,91 @@ export class Level {
|
|||||||
this.pending_undo.level_props[key] = this[key];
|
this.pending_undo.level_props[key] = this[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.p1_input = p1_input;
|
||||||
|
|
||||||
// Used for various tic-local effects; don't need to be undoable
|
// Used for various tic-local effects; don't need to be undoable
|
||||||
// TODO maybe this should be undone anyway so rewind looks better?
|
// TODO maybe this should be undone anyway so rewind looks better?
|
||||||
this.player.is_blocked = false;
|
this.player.is_blocked = false;
|
||||||
|
|
||||||
this.sfx.set_player_position(this.player.cell);
|
this.sfx.set_player_position(this.player.cell);
|
||||||
|
|
||||||
// CC2 wiring runs every frame, not every tic, so we need to do it three times, but dealing
|
this._do_decision_pass(false);
|
||||||
// with it is delicate. We want the result of a button press to draw, but not last longer
|
this._do_action_pass();
|
||||||
// than intended, so we only want one update between the end of the cooldown pass and the
|
this._do_cooldown_pass();
|
||||||
// end of the tic. That means the other two have to go here. When a level starts, there
|
this.update_wiring();
|
||||||
// are only two wiring updates before everything gets its first chance to move, so we skip
|
this._do_decision_pass(false);
|
||||||
// the very first one here.
|
this._do_action_pass();
|
||||||
if (this.tic_counter !== 0) {
|
this._do_cooldown_pass();
|
||||||
this.update_wiring();
|
this.update_wiring();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finish_tic(p1_input) {
|
||||||
|
this.p1_input = p1_input;
|
||||||
|
this._do_decision_pass(true);
|
||||||
|
|
||||||
|
// In the event that the player is sliding (and thus not deliberately moving) or has
|
||||||
|
// stopped, remember their current movement direction here, too.
|
||||||
|
// This is hokey, and doing it here is even hokier, but it seems to match CC2 behavior.
|
||||||
|
if (this.player.movement_cooldown > 0) {
|
||||||
|
this.remember_player_move(this.player.direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._do_action_pass();
|
||||||
|
this._do_cooldown_pass();
|
||||||
this.update_wiring();
|
this.update_wiring();
|
||||||
|
|
||||||
|
// 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?
|
||||||
|
let p = 0;
|
||||||
|
for (let i = 0, l = this.actors.length; i < l; i++) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
// While we're here, delete this guy
|
||||||
|
delete actor.cooldown_delay_hack;
|
||||||
|
|
||||||
|
if (actor.cell) {
|
||||||
|
if (p !== i) {
|
||||||
|
this.actors[p] = actor;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let local_p = p;
|
||||||
|
this.pending_undo.push(() => this.actors.splice(local_p, 0, actor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.actors.length = p;
|
||||||
|
|
||||||
|
// Possibly switch players
|
||||||
|
// FIXME not correct
|
||||||
|
/*
|
||||||
|
if (swap_player1) {
|
||||||
|
this.player_index += 1;
|
||||||
|
this.player_index %= this.players.length;
|
||||||
|
this.player = this.players[this.player_index];
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
this.previous_input = this.p1_input;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
this.tic_counter += 1;
|
||||||
|
if (this.time_remaining !== null && ! this.timer_paused) {
|
||||||
|
this.time_remaining -= 1;
|
||||||
|
if (this.time_remaining <= 0) {
|
||||||
|
this.fail('time');
|
||||||
|
}
|
||||||
|
else if (this.time_remaining % 20 === 0 && this.time_remaining < 30 * 20) {
|
||||||
|
this.sfx.play_once('tick');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
_do_cooldown_pass() {
|
||||||
// FIRST PASS: actors tick their cooldowns, finish their movement, and possibly step on
|
// 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
|
// 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)
|
// much easier, and doing it as a separate pass from /starting/ movement (unlike Lynx)
|
||||||
@ -631,146 +699,9 @@ export class Level {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (actor.just_stepped_on_teleporter) {
|
if (actor.just_stepped_on_teleporter) {
|
||||||
this.attempt_teleport(actor, actor === this.player ? p1_input : null);
|
this.attempt_teleport(actor, actor === this.player ? this.p1_input : null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Here's the third.
|
|
||||||
this.update_wiring();
|
|
||||||
}
|
|
||||||
|
|
||||||
finish_tic(p1_input) {
|
|
||||||
// SECOND PASS: actors decide their upcoming movement simultaneously
|
|
||||||
for (let i = this.actors.length - 1; i >= 0; i--) {
|
|
||||||
let actor = this.actors[i];
|
|
||||||
|
|
||||||
// Clear any old decisions ASAP. Note that this prop is only used internally within a
|
|
||||||
// single tic, so it doesn't need to be undoable
|
|
||||||
actor.decision = null;
|
|
||||||
|
|
||||||
if (! actor.cell)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (actor.movement_cooldown > 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (actor === this.player) {
|
|
||||||
this.make_player_decision(actor, p1_input);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.make_actor_decision(actor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// THIRD PASS: everyone actually moves
|
|
||||||
let swap_player1 = 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check this again, since an earlier pass may have caused us to start moving
|
|
||||||
if (actor.movement_cooldown > 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Check for special player actions, which can only happen when not moving
|
|
||||||
// FIXME if you press a key while moving it should happen as soon as you stop (assuming
|
|
||||||
// the key is still held down)
|
|
||||||
// FIXME cc2 seems to rely on key repeat for this; the above is true, but also, 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
|
|
||||||
if (actor === this.player) {
|
|
||||||
let new_input = p1_input & ~this.previous_input;
|
|
||||||
if (new_input & INPUT_BITS.cycle) {
|
|
||||||
this.cycle_inventory(this.player);
|
|
||||||
}
|
|
||||||
if (new_input & INPUT_BITS.drop) {
|
|
||||||
this.drop_item(this.player);
|
|
||||||
}
|
|
||||||
if (new_input & INPUT_BITS.swap) {
|
|
||||||
// This is delayed until the end of the tic to avoid screwing up anything
|
|
||||||
// checking this.player
|
|
||||||
swap_player1 = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! actor.decision)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Actor is allowed to move, so do so
|
|
||||||
let old_cell = actor.cell;
|
|
||||||
let success = this.attempt_step(actor, actor.decision);
|
|
||||||
|
|
||||||
if (! success && actor.slide_mode === 'ice') {
|
|
||||||
this._handle_slide_bonk(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');
|
|
||||||
actor.is_blocked = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the event that the player is sliding (and thus not deliberately moving) or has
|
|
||||||
// stopped, remember their current movement direction here, too.
|
|
||||||
// This is hokey, and doing it here is even hokier, but it seems to match CC2 behavior.
|
|
||||||
if (this.player.movement_cooldown > 0) {
|
|
||||||
this.remember_player_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. not strictly
|
|
||||||
// necessary, either; maybe only do it every few tics?
|
|
||||||
let p = 0;
|
|
||||||
for (let i = 0, l = this.actors.length; i < l; i++) {
|
|
||||||
let actor = this.actors[i];
|
|
||||||
// While we're here, delete this guy
|
|
||||||
delete actor.cooldown_delay_hack;
|
|
||||||
|
|
||||||
if (actor.cell) {
|
|
||||||
if (p !== i) {
|
|
||||||
this.actors[p] = actor;
|
|
||||||
}
|
|
||||||
p++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let local_p = p;
|
|
||||||
this.pending_undo.push(() => this.actors.splice(local_p, 0, actor));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.actors.length = p;
|
|
||||||
|
|
||||||
// Possibly switch players
|
|
||||||
// FIXME not correct
|
|
||||||
if (swap_player1) {
|
|
||||||
this.player_index += 1;
|
|
||||||
this.player_index %= this.players.length;
|
|
||||||
this.player = this.players[this.player_index];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.previous_input = p1_input;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
this.tic_counter += 1;
|
|
||||||
if (this.time_remaining !== null && ! this.timer_paused) {
|
|
||||||
this.time_remaining -= 1;
|
|
||||||
if (this.time_remaining <= 0) {
|
|
||||||
this.fail('time');
|
|
||||||
}
|
|
||||||
else if (this.time_remaining % 20 === 0 && this.time_remaining < 30 * 20) {
|
|
||||||
this.sfx.play_once('tick');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.commit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_extract_player_directions(input) {
|
_extract_player_directions(input) {
|
||||||
@ -834,6 +765,15 @@ export class Level {
|
|||||||
|
|
||||||
if (actor.slide_mode === 'force') {
|
if (actor.slide_mode === 'force') {
|
||||||
this._set_tile_prop(actor, 'last_move_was_force', true);
|
this._set_tile_prop(actor, 'last_move_was_force', true);
|
||||||
|
if (! try_direction(actor.direction, 'move') && actor.cell.get_terrain().type.name === 'force_floor_all') {
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
let direction = this.get_force_floor_direction();
|
||||||
|
if (try_direction(direction, 'move') || i === 2) {
|
||||||
|
actor.decision = direction;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (actor.slide_mode === 'ice') {
|
else if (actor.slide_mode === 'ice') {
|
||||||
// A sliding player that bonks into a wall still needs to turn around, but in this
|
// A sliding player that bonks into a wall still needs to turn around, but in this
|
||||||
@ -901,11 +841,23 @@ export class Level {
|
|||||||
// If we're overriding a force floor but the direction we're moving in is blocked, the
|
// If we're overriding a force floor but the direction we're moving in is blocked, the
|
||||||
// force floor takes priority (and we've already bumped the wall(s))
|
// force floor takes priority (and we've already bumped the wall(s))
|
||||||
if (actor.slide_mode === 'force' && ! open) {
|
if (actor.slide_mode === 'force' && ! open) {
|
||||||
actor.decision = actor.direction;
|
|
||||||
this._set_tile_prop(actor, 'last_move_was_force', true);
|
this._set_tile_prop(actor, 'last_move_was_force', true);
|
||||||
// Be sure to invoke this hack if we're standing on an RFF
|
// Be sure to invoke this hack if we're standing on an RFF
|
||||||
|
|
||||||
|
if (actor.cell.get_terrain().type.name === 'force_floor_all') {
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
let direction = this.get_force_floor_direction();
|
||||||
|
if (try_direction(direction, 'move') || i === 2) {
|
||||||
|
actor.decision = direction;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
actor.decision = actor.direction;
|
||||||
this._handle_slide_bonk(actor);
|
this._handle_slide_bonk(actor);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
// Otherwise this is 100% a conscious move so we lose our override power next tic
|
// Otherwise this is 100% a conscious move so we lose our override power next tic
|
||||||
this._set_tile_prop(actor, 'last_move_was_force', false);
|
this._set_tile_prop(actor, 'last_move_was_force', false);
|
||||||
@ -978,6 +930,262 @@ export class Level {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_do_decision_pass(tic_aligned) {
|
||||||
|
// SECOND PASS: actors decide their upcoming movement simultaneously
|
||||||
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
|
||||||
|
// Clear any old decisions ASAP. Note that this prop is only used internally within a
|
||||||
|
// single tic, so it doesn't need to be undoable
|
||||||
|
actor.decision = null;
|
||||||
|
|
||||||
|
if (! actor.cell)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (actor.movement_cooldown > 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (! tic_aligned && ! actor.slide_mode)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (actor.slide_mode && actor.cell.get_terrain().type.name === 'force_floor_all') {
|
||||||
|
this.set_actor_direction(actor, this.get_force_floor_direction());
|
||||||
|
}
|
||||||
|
if (actor === this.player) {
|
||||||
|
this.make_player_decision(actor, this.p1_input);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.make_actor_decision(actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_do_action_pass() {
|
||||||
|
// THIRD PASS: everyone actually moves
|
||||||
|
let swap_player1 = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check this again, since an earlier pass may have caused us to start moving
|
||||||
|
if (actor.movement_cooldown > 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check for special player actions, which can only happen when not moving
|
||||||
|
// FIXME if you press a key while moving it should happen as soon as you stop (assuming
|
||||||
|
// the key is still held down)
|
||||||
|
// FIXME cc2 seems to rely on key repeat for this; the above is true, but also, 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
|
||||||
|
if (actor === this.player) {
|
||||||
|
let new_input = this.p1_input & ~this.previous_input;
|
||||||
|
if (new_input & INPUT_BITS.cycle) {
|
||||||
|
this.cycle_inventory(this.player);
|
||||||
|
}
|
||||||
|
if (new_input & INPUT_BITS.drop) {
|
||||||
|
this.drop_item(this.player);
|
||||||
|
}
|
||||||
|
if (new_input & INPUT_BITS.swap) {
|
||||||
|
// This is delayed until the end of the tic to avoid screwing up anything
|
||||||
|
// checking this.player
|
||||||
|
swap_player1 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! actor.decision)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Actor is allowed to move, so do so
|
||||||
|
let old_cell = actor.cell;
|
||||||
|
let success = this.attempt_step(actor, actor.decision);
|
||||||
|
|
||||||
|
if (! success && actor.slide_mode === 'ice') {
|
||||||
|
this._handle_slide_bonk(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');
|
||||||
|
actor.is_blocked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_do_cooldown_pass() {
|
||||||
|
// 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
|
||||||
|
// 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
|
||||||
|
// advance the RNG, so for replay compatibility it needs to be in reverse order too.)
|
||||||
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
// Actors with no cell were destroyed
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
if (! actor.cell)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (actor.just_stepped_on_teleporter) {
|
||||||
|
this.attempt_teleport(actor, actor === this.player ? this.p1_input : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_do_turn_phase(allow_decision) {
|
||||||
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
|
||||||
|
// Clear any old decisions ASAP. Note that this prop is only used internally within a
|
||||||
|
// single tic, so it doesn't need to be undoable
|
||||||
|
actor.decision = null;
|
||||||
|
|
||||||
|
if (! actor.cell)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (actor.movement_cooldown <= 0 && (allow_decision || actor.slide_mode)) {
|
||||||
|
if (actor === this.player) {
|
||||||
|
this.make_player_decision(actor, this.p1_input);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.make_actor_decision(actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.take_actor_turn(actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
take_actor_turn(actor) {
|
||||||
|
let success = false;
|
||||||
|
|
||||||
|
if (! actor.cell)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// FIXME obviously this becomes on_frame
|
||||||
|
if (actor.type.on_tic) {
|
||||||
|
actor.type.on_tic(actor, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check this again, since an earlier pass may have caused us to start moving
|
||||||
|
if (actor.movement_cooldown <= 0) {
|
||||||
|
|
||||||
|
// Check for special player actions, which can only happen when not moving
|
||||||
|
// FIXME if you press a key while moving it should happen as soon as you stop (assuming
|
||||||
|
// the key is still held down)
|
||||||
|
// FIXME cc2 seems to rely on key repeat for this; the above is true, but also, 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
|
||||||
|
if (actor === this.player) {
|
||||||
|
let new_input = this.p1_input & ~this.previous_input;
|
||||||
|
if (new_input & INPUT_BITS.cycle) {
|
||||||
|
this.cycle_inventory(this.player);
|
||||||
|
}
|
||||||
|
if (new_input & INPUT_BITS.drop) {
|
||||||
|
this.drop_item(this.player);
|
||||||
|
}
|
||||||
|
if (new_input & INPUT_BITS.swap) {
|
||||||
|
// This is delayed until the end of the tic to avoid screwing up anything
|
||||||
|
// checking this.player
|
||||||
|
swap_player1 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.decision) {
|
||||||
|
// Actor is allowed to move, so do so
|
||||||
|
let old_cell = actor.cell;
|
||||||
|
success = this.attempt_step(actor, actor.decision);
|
||||||
|
|
||||||
|
if (! success && actor.slide_mode === 'ice') {
|
||||||
|
this._handle_slide_bonk(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');
|
||||||
|
actor.is_blocked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor.movement_cooldown > 0) {
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// FIXME can probably clean this up a decent bit now
|
// FIXME can probably clean this up a decent bit now
|
||||||
_handle_slide_bonk(actor) {
|
_handle_slide_bonk(actor) {
|
||||||
if (actor.slide_mode === 'ice') {
|
if (actor.slide_mode === 'ice') {
|
||||||
@ -1119,14 +1327,16 @@ export class Level {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._set_tile_prop(actor, 'previous_cell', actor.cell);
|
this._set_tile_prop(actor, 'previous_cell', actor.cell);
|
||||||
this._set_tile_prop(actor, 'movement_cooldown', speed);
|
this._set_tile_prop(actor, 'movement_cooldown', speed * 3);
|
||||||
this._set_tile_prop(actor, 'movement_speed', speed);
|
this._set_tile_prop(actor, 'movement_speed', speed * 3);
|
||||||
this.move_to(actor, goal_cell, speed);
|
this.move_to(actor, goal_cell, speed);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
attempt_out_of_turn_step(actor, direction) {
|
attempt_out_of_turn_step(actor, direction) {
|
||||||
|
actor.decision = direction;
|
||||||
|
return this.take_actor_turn(actor);
|
||||||
if (this.attempt_step(actor, direction)) {
|
if (this.attempt_step(actor, direction)) {
|
||||||
// Here's the problem.
|
// Here's the problem.
|
||||||
// In CC2, cooldown is measured in frames, not tics, and it decrements every frame, not
|
// In CC2, cooldown is measured in frames, not tics, and it decrements every frame, not
|
||||||
@ -1149,7 +1359,7 @@ export class Level {
|
|||||||
// XXX that's still not perfect; if actor X is tic-misaligned, like if there's a chain
|
// XXX that's still not perfect; if actor X is tic-misaligned, like if there's a chain
|
||||||
// of 3 or more actors cloning directly onto red buttons for other cloners, then this
|
// of 3 or more actors cloning directly onto red buttons for other cloners, then this
|
||||||
// cannot possibly work
|
// cannot possibly work
|
||||||
actor.cooldown_delay_hack = 1;
|
//actor.cooldown_delay_hack = 1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@ -781,7 +781,7 @@ const TILE_TYPES = {
|
|||||||
// TODO ms: this is random, and an acting wall to monsters (!)
|
// TODO ms: this is random, and an acting wall to monsters (!)
|
||||||
// TODO lynx/cc2 check this at decision time, which may affect ordering
|
// TODO lynx/cc2 check this at decision time, which may affect ordering
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
level.set_actor_direction(other, level.get_force_floor_direction());
|
//level.set_actor_direction(other, level.get_force_floor_direction());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
slime: {
|
slime: {
|
||||||
@ -2496,7 +2496,7 @@ const TILE_TYPES = {
|
|||||||
is_actor: true,
|
is_actor: true,
|
||||||
collision_mask: 0,
|
collision_mask: 0,
|
||||||
blocks_collision: COLLISION.player,
|
blocks_collision: COLLISION.player,
|
||||||
ttl: 6,
|
ttl: 16,
|
||||||
// If anything else even begins to step on an animation, it's erased
|
// 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?
|
// FIXME possibly erased too fast; cc2 shows it briefly? could i get away with on_arrive here?
|
||||||
on_approach(me, level, other) {
|
on_approach(me, level, other) {
|
||||||
@ -2508,7 +2508,7 @@ const TILE_TYPES = {
|
|||||||
is_actor: true,
|
is_actor: true,
|
||||||
collision_mask: 0,
|
collision_mask: 0,
|
||||||
blocks_collision: COLLISION.player,
|
blocks_collision: COLLISION.player,
|
||||||
ttl: 6,
|
ttl: 16,
|
||||||
on_approach(me, level, other) {
|
on_approach(me, level, other) {
|
||||||
level.remove_tile(me);
|
level.remove_tile(me);
|
||||||
},
|
},
|
||||||
@ -2520,7 +2520,7 @@ const TILE_TYPES = {
|
|||||||
collision_mask: 0,
|
collision_mask: 0,
|
||||||
blocks_collision: 0,
|
blocks_collision: 0,
|
||||||
// determined experimentally
|
// determined experimentally
|
||||||
ttl: 12,
|
ttl: 36,
|
||||||
},
|
},
|
||||||
// Custom VFX (identical function, but different aesthetic)
|
// Custom VFX (identical function, but different aesthetic)
|
||||||
splash_slime: {
|
splash_slime: {
|
||||||
@ -2528,7 +2528,7 @@ const TILE_TYPES = {
|
|||||||
is_actor: true,
|
is_actor: true,
|
||||||
collision_mask: 0,
|
collision_mask: 0,
|
||||||
blocks_collision: COLLISION.player,
|
blocks_collision: COLLISION.player,
|
||||||
ttl: 6,
|
ttl: 16,
|
||||||
on_approach(me, level, other) {
|
on_approach(me, level, other) {
|
||||||
level.remove_tile(me);
|
level.remove_tile(me);
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user