Merge Lexy/Lynx loops; add compat for separated teleport phase

This commit is contained in:
Eevee (Evelyn Woods) 2021-05-17 19:12:04 -06:00
parent a6aaaa7266
commit ae8b42e0c9
2 changed files with 60 additions and 85 deletions

View File

@ -151,9 +151,13 @@ export const COMPAT_FLAGS = [
// Core // Core
{ {
key: 'use_lynx_loop', key: 'allow_double_cooldowns',
label: "Game uses the Lynx-style update loop", label: "Actors may cooldown twice in one tic",
rulesets: new Set(['steam', 'steam-strict', 'lynx', 'ms']), rulesets: new Set(['steam', 'steam-strict', 'lynx']),
}, {
key: 'no_separate_idle_phase',
label: "Actors teleport immediately after moving",
rulesets: new Set(['steam', 'steam-strict']),
}, { }, {
key: 'player_moves_last', key: 'player_moves_last',
label: "Player always moves last", label: "Player always moves last",

View File

@ -269,7 +269,7 @@ export class Level extends LevelInterface {
} }
get update_rate() { get update_rate() {
if (this.compat.use_lynx_loop && this.compat.emulate_60fps) { if (this.compat.emulate_60fps) {
return 1; return 1;
} }
else { else {
@ -767,21 +767,16 @@ export class Level extends LevelInterface {
this._do_init_phase(); this._do_init_phase();
this._set_p1_input(p1_input); this._set_p1_input(p1_input);
if (this.compat.use_lynx_loop) { if (this.compat.emulate_60fps) {
if (this.compat.emulate_60fps) { this._advance_tic_lynx60();
this._advance_tic_lynx60();
}
else {
this._advance_tic_lynx();
}
} }
else { else {
this._advance_tic_lexy(); this._advance_tic_lynx();
} }
} }
// Default loop: run at 20 tics per second, split things into some more loops // Lynx/Lexy loop: 20 tics per second
_advance_tic_lexy() { _advance_tic_lynx() {
// Under CC2 rules, there are two wire updates at the very beginning of the game before the // Under CC2 rules, there are two wire updates at the very beginning of the game before the
// player can actually move. That means the first tic has five wire phases total. // player can actually move. That means the first tic has five wire phases total.
// FIXME this breaks item bestowal contraptions that immediately flip a force floor, since // FIXME this breaks item bestowal contraptions that immediately flip a force floor, since
@ -792,50 +787,7 @@ export class Level extends LevelInterface {
} }
this._do_decision_phase(); this._do_decision_phase();
this._do_action_phase(3);
// 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);
}
// 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
// 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.type.ttl) {
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. (CC2 doesn't need to do
// this because blocks you're pushing are always a frame ahead of you anyway.)
// This is also where we handle tiles with persistent standing behavior.
for (let i = this.actors.length - 1; i >= 0; i--) {
let actor = this.actors[i];
if (! actor.cell)
continue;
if (actor.type.ttl)
continue;
this._do_actor_idle(actor);
}
this._swap_players();
this._do_post_actor_phase();
// Wire updates every frame, which means thrice per tic // Wire updates every frame, which means thrice per tic
this._do_wire_phase(); this._do_wire_phase();
@ -845,36 +797,21 @@ export class Level extends LevelInterface {
this._do_cleanup_phase(); this._do_cleanup_phase();
} }
// Lynx loop: everyone decides, then everyone moves/cools in a single pass
_advance_tic_lynx() {
this._do_decision_phase();
this._do_combined_action_phase(3);
this._do_post_actor_phase();
this._do_wire_phase();
this._do_wire_phase();
this._do_wire_phase();
this._do_cleanup_phase();
}
// CC2 loop: similar to the Lynx loop, but run three times per tic, and non-forced decisions can // CC2 loop: similar to the Lynx loop, but run three times per tic, and non-forced decisions can
// only be made every third frame // only be made every third frame
_advance_tic_lynx60() { _advance_tic_lynx60() {
this._do_decision_phase(true); this._do_decision_phase(true);
this._do_combined_action_phase(1); this._do_action_phase(1);
this._do_post_actor_phase();
this._do_wire_phase(); this._do_wire_phase();
this.frame_offset = 1; this.frame_offset = 1;
this._do_decision_phase(true); this._do_decision_phase(true);
this._do_combined_action_phase(1); this._do_action_phase(1);
this._do_post_actor_phase();
this._do_wire_phase(); this._do_wire_phase();
this.frame_offset = 2; this.frame_offset = 2;
this._do_decision_phase(); this._do_decision_phase();
this._do_combined_action_phase(1); this._do_action_phase(1);
this._do_post_actor_phase();
this._do_wire_phase(); this._do_wire_phase();
this.frame_offset = 0; this.frame_offset = 0;
@ -884,8 +821,8 @@ export class Level extends LevelInterface {
// Attempt to advance by one FRAME at a time. Primarily useful for running 60 FPS mode at, // Attempt to advance by one FRAME at a time. Primarily useful for running 60 FPS mode at,
// well, 60 FPS. // well, 60 FPS.
advance_frame(p1_input) { advance_frame(p1_input) {
if (this.compat.use_lynx_loop && this.compat.emulate_60fps) { if (this.compat.emulate_60fps) {
// Lynx 60, i.e. CC2 // CC2
if (this.frame_offset === 0) { if (this.frame_offset === 0) {
this._do_init_phase(p1_input); this._do_init_phase(p1_input);
} }
@ -893,8 +830,7 @@ export class Level extends LevelInterface {
let is_decision_frame = this.frame_offset === 2; let is_decision_frame = this.frame_offset === 2;
this._do_decision_phase(! is_decision_frame); this._do_decision_phase(! is_decision_frame);
this._do_combined_action_phase(1); this._do_action_phase(1);
this._do_post_actor_phase();
this._do_wire_phase(); this._do_wire_phase();
if (this.frame_offset === 2) { if (this.frame_offset === 2) {
@ -902,7 +838,7 @@ export class Level extends LevelInterface {
} }
} }
else { else {
// This is either Lexy mode or Lynx mode, and either way we run at 20 tps // We're running at 20 tps, which means only one update on the first frame
if (this.frame_offset === 0) { if (this.frame_offset === 0) {
this.advance_tic(p1_input); this.advance_tic(p1_input);
} }
@ -1019,7 +955,44 @@ export class Level extends LevelInterface {
} }
} }
// Lynx's combined action phase: each actor attempts to move, then cools down, in order _do_action_phase(cooldown) {
if (this.compat.no_separate_idle_phase) {
this._do_combined_action_phase(cooldown);
}
else {
this._do_separated_action_phase(cooldown);
}
// Post-action stuff
this._swap_players();
this._do_post_actor_phase();
}
// Lynx + Lexy action phase: move and cool down in one loop, idle in another
_do_separated_action_phase(cooldown) {
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);
if (actor.type.ttl)
continue;
this._do_actor_cooldown(actor, cooldown);
}
for (let i = this.actors.length - 1; i >= 0; i--) {
let actor = this.actors[i];
if (! actor.cell)
continue;
if (actor.type.ttl)
continue;
this._do_actor_idle(actor);
}
}
// CC2 action phase: move, cool down, and idle all in one loop
_do_combined_action_phase(cooldown) { _do_combined_action_phase(cooldown) {
for (let i = this.actors.length - 1; i >= 0; i--) { for (let i = this.actors.length - 1; i >= 0; i--) {
let actor = this.actors[i]; let actor = this.actors[i];
@ -1033,8 +1006,6 @@ export class Level extends LevelInterface {
this._do_actor_cooldown(actor, cooldown); this._do_actor_cooldown(actor, cooldown);
this._do_actor_idle(actor); this._do_actor_idle(actor);
} }
this._swap_players();
} }
// Have an actor attempt to move // Have an actor attempt to move
@ -1953,7 +1924,7 @@ export class Level extends LevelInterface {
_do_extra_cooldown(actor) { _do_extra_cooldown(actor) {
this._do_actor_cooldown(actor, this.update_rate); this._do_actor_cooldown(actor, this.update_rate);
// Only Lexy has double-cooldown protection // Only Lexy has double-cooldown protection
if (! this.compat.use_lynx_loop) { if (! this.compat.allow_double_cooldowns) {
this._set_tile_prop(actor, 'last_extra_cooldown_tic', this.tic_counter); this._set_tile_prop(actor, 'last_extra_cooldown_tic', this.tic_counter);
} }
} }