Arrange the compat flags into categories & show compat icon in main UI

This commit is contained in:
Eevee (Evelyn Woods) 2024-04-24 12:30:59 -06:00
parent 0efbefb999
commit 5a17b9022d
4 changed files with 221 additions and 189 deletions

View File

@ -135,7 +135,7 @@
<h1><a href="https://github.com/eevee/lexys-labyrinth">Lexy's Labyrinth</a></h1> <h1><a href="https://github.com/eevee/lexys-labyrinth">Lexy's Labyrinth</a></h1>
<p>— an <a href="https://github.com/eevee/lexys-labyrinth">open source</a> game by <a href="https://eev.ee/">eevee</a></p> <p>— an <a href="https://github.com/eevee/lexys-labyrinth">open source</a> game by <a href="https://eev.ee/">eevee</a></p>
<nav> <nav>
<button id="main-compat" type="button">mode: <output>lexy</output></button> <button id="main-compat" type="button"><img src="icons/compat-lexy.png" alt=""> <output>lexy</output></button>
<button id="main-options" type="button">options</button> <button id="main-options" type="button">options</button>
</nav> </nav>
</header> </header>

View File

@ -135,132 +135,126 @@ export const COMPAT_RULESET_LABELS = {
}; };
export const COMPAT_RULESET_ORDER = ['lexy', 'steam', 'steam-strict', 'lynx', 'ms', 'custom']; export const COMPAT_RULESET_ORDER = ['lexy', 'steam', 'steam-strict', 'lynx', 'ms', 'custom'];
// FIXME some of the names of the flags themselves kinda suck // FIXME some of the names of the flags themselves kinda suck
export const COMPAT_FLAGS = [ // TODO some ms compat things that wouldn't be too hard to add:
// Level loading // - walkers choose a random /unblocked/ direction, not just a random direction
// TODO? /strictly/ speaking, these should be turned on for lynx+ms/lynx respectively, but then i'd // - (boosting) player cooldown is /zero/ after ending a slide
// have to also alter the behavior of the corresponding terrain, which seems kind of silly // - cleats allow walking through ice corner walls while standing on them
{ // - blocks can be pushed through thin walls + ice corners
export const COMPAT_FLAG_CATEGORIES = [{
title: "Level loading",
flags: [{
key: 'no_auto_convert_ccl_popwalls', key: 'no_auto_convert_ccl_popwalls',
label: "Recessed walls under actors in CCL levels are left alone", label: "Recessed walls under actors are not auto-converted in CCL levels",
rulesets: new Set(['steam-strict', 'lynx', 'ms']), rulesets: new Set(['steam-strict', 'lynx', 'ms']),
}, { }, {
key: 'no_auto_convert_ccl_blue_walls', key: 'no_auto_convert_ccl_blue_walls',
label: "Blue walls under blocks in CCL levels are left alone", label: "Blue walls under blocks are not auto-converted in CCL levels",
rulesets: new Set(['steam-strict', 'lynx', 'ms']), rulesets: new Set(['steam-strict', 'lynx', 'ms']),
}, }],
}, {
// Core title: "Actor behavior",
{ flags: [{
key: 'allow_double_cooldowns', key: 'emulate_60fps',
label: "Actors may cooldown twice in one tic", label: "Actors update at 60 FPS",
rulesets: new Set(['steam', 'steam-strict', 'lynx']), rulesets: new Set(['steam', 'steam-strict']),
}, { }, {
key: 'no_separate_idle_phase', key: 'no_separate_idle_phase',
label: "Actors teleport immediately after moving", label: "Actors teleport immediately after moving",
rulesets: new Set(['steam', 'steam-strict']), rulesets: new Set(['steam', 'steam-strict']),
}, {
key: 'allow_double_cooldowns',
label: "Actors may move forwards twice in one tic",
rulesets: new Set(['steam', 'steam-strict', 'lynx']),
}, { }, {
key: 'player_moves_last', key: 'player_moves_last',
label: "Players always move last", label: "Players always update last",
rulesets: new Set(['lynx', 'ms']), rulesets: new Set(['lynx']),
}, {
key: 'reuse_actor_slots',
label: "New actors reuse slots in the actor list",
rulesets: new Set(['lynx']),
}, { }, {
key: 'player_protected_by_items', key: 'player_protected_by_items',
label: "Players can't be trampled when standing on items", label: "Players can't be trampled while standing on items",
rulesets: new Set(['lynx']),
}, {
key: 'force_lynx_animation_lengths',
label: "Animations play at their slower Lynx duration",
rulesets: new Set(['lynx']), rulesets: new Set(['lynx']),
}, { }, {
// Note that this requires no_early_push as well // Note that this requires no_early_push as well
key: 'player_safe_at_decision_time', key: 'player_safe_at_decision_time',
label: "Players can't be trampled at decision time", label: "Players can't be trampled at decision time",
rulesets: new Set(['lynx']), rulesets: new Set(['lynx', 'ms']),
}, { }, {
key: 'emulate_60fps', key: 'bonking_isnt_instant',
label: "Game runs at 60 FPS", label: "Bonking while sliding doesn't apply instantly",
rulesets: new Set(['steam', 'steam-strict']), rulesets: new Set(['lynx', 'ms']),
}, {
key: 'reuse_actor_slots',
label: "Game reuses slots in the actor list",
rulesets: new Set(['lynx']),
}, {
key: 'force_lynx_animation_lengths',
label: "Animations use Lynx duration",
rulesets: new Set(['lynx']),
}, { }, {
key: 'actors_move_instantly', key: 'actors_move_instantly',
label: "Movement happens instantly", label: "Movement is instant",
rulesets: new Set(['ms']),
},
// Tiles
{
key: 'rff_actually_random',
label: "Random force floors are actually random",
rulesets: new Set(['ms']), rulesets: new Set(['ms']),
}],
}, { }, {
key: 'no_backwards_override', title: "Monsters",
label: "Players can't override backwards on a force floor", flags: [{
rulesets: new Set(['lynx']), // TODO ms needs "player doesn't block monsters", but tbh that's kind of how it should work
}, { // anyway, especially in combination with the ankh
key: 'traps_like_lynx', // TODO? in lynx they ignore the button while in motion too
label: "Traps eject faster, and even when already open", // TODO what about in a trap, in every game??
rulesets: new Set(['lynx']), // TODO what does ms do when a tank is on ice or a ff? wiki's description is wacky
}, { // TODO yellow tanks seem to have memory too??
key: 'popwalls_pop_on_arrive', key: 'tanks_always_obey_button',
label: "Recessed walls activate when stepped on", label: "Blue tanks obey blue buttons even on clone machines",
rulesets: new Set(['lynx', 'ms']),
}, {
key: 'blue_floors_vanish_on_arrive',
label: "Fake blue walls vanish when stepped on",
rulesets: new Set(['lynx']),
}, {
key: 'green_teleports_can_fail',
label: "Green teleporters sometimes fail",
rulesets: new Set(['steam-strict']),
},
// Items
{
key: 'bombs_detonate_on_arrive',
label: "Mines detonate only when stepped on",
rulesets: new Set(['lynx', 'ms']),
}, {
key: 'bombs_immediately_detonate_under_players',
label: "Mines under players detonate at level start",
rulesets: new Set(['steam-strict']), rulesets: new Set(['steam-strict']),
}, { }, {
key: 'cloned_bowling_balls_can_be_lost', key: 'tanks_ignore_button_while_moving',
label: "Bowling balls on cloners are destroyed when fired at point blank", label: "Blue tanks ignore blue buttons while moving",
rulesets: new Set(['lynx']),
}, {
key: 'blobs_use_tw_prng',
label: "Blobs use the Lynx RNG",
rulesets: new Set(['lynx']),
}, {
key: 'teeth_target_internal_position',
label: "Teeth pursue the cell the player is moving into",
rulesets: new Set(['lynx']),
}, {
key: 'rff_blocks_monsters',
label: "Monsters cannot step on random force floors",
rulesets: new Set(['ms']),
}, {
key: 'fire_allows_most_monsters',
label: "Monsters can walk into fire, except for bugs and walkers",
rulesets: new Set(['ms']),
}],
}, {
title: "Blocks",
flags: [{
key: 'use_legacy_hooking',
label: "Pulling blocks with the hook happens earlier, and may prevent moving",
rulesets: new Set(['steam', 'steam-strict']),
}, {
key: 'no_directly_pushing_sliding_blocks',
label: "Pushing sliding blocks queues a move, rather than moving them right away",
rulesets: new Set(['steam', 'steam-strict']),
}, {
key: 'emulate_spring_mining',
label: "Pushing a block off a recessed wall may cause you to move into the resulting wall",
rulesets: new Set(['steam-strict']), rulesets: new Set(['steam-strict']),
}, { }, {
key: 'monsters_ignore_keys',
label: "Monsters completely ignore keys",
rulesets: new Set(['ms']),
},
// Blocks
{
key: 'no_early_push', key: 'no_early_push',
label: "Pushing blocks happens at move time (block slapping is disabled)", label: "Pushing blocks happens at move time (block slapping is disabled)",
// XXX wait but the DEFAULT behavior allows block slapping, which lynx has, so why is lynx listed here? // XXX wait but the DEFAULT behavior allows block slapping, which lynx has, so why is lynx listed here?
rulesets: new Set(['lynx', 'ms']), rulesets: new Set(['lynx', 'ms']),
}, {
key: 'use_legacy_hooking',
label: "Pulling blocks with the hook happens at decision time",
rulesets: new Set(['steam', 'steam-strict']),
}, {
key: 'no_directly_pushing_sliding_blocks',
label: "Don't directly push sliding blocks",
rulesets: new Set(['steam', 'steam-strict']),
}, { }, {
key: 'use_pgchip_ice_blocks', key: 'use_pgchip_ice_blocks',
label: "Ice blocks emulate pgchip rules", label: "Ice blocks use pgchip rules",
rulesets: new Set(['ms']), rulesets: new Set(['ms']),
}, { }, {
key: 'allow_pushing_blocks_off_faux_walls', key: 'allow_pushing_blocks_off_faux_walls',
label: "Blocks may be pushed off of blue (fake), invisible, and revealing walls", label: "Blocks may be pushed off of blue (fake), invisible, and revealing walls",
rulesets: new Set(['lynx']), rulesets: new Set(['lynx']),
}, {
key: 'emulate_spring_mining',
label: "Spring mining is possible",
rulesets: new Set(['steam-strict']),
}, { }, {
key: 'block_splashes_dont_block', key: 'block_splashes_dont_block',
label: "Block splashes don't block the player", label: "Block splashes don't block the player",
@ -271,50 +265,64 @@ export const COMPAT_FLAGS = [
label: "Flicking is possible", label: "Flicking is possible",
rulesets: new Set(['ms']), rulesets: new Set(['ms']),
*/ */
}, }],
}, {
// Monsters title: "Terrain",
{ flags: [{
// TODO? in lynx they ignore the button while in motion too key: 'green_teleports_can_fail',
// TODO what about in a trap, in every game?? label: "Green teleporters sometimes fail",
// TODO what does ms do when a tank is on ice or a ff? wiki's description is wacky
// TODO yellow tanks seem to have memory too??
key: 'tanks_always_obey_button',
label: "Blue tanks on cloners obey blue buttons",
rulesets: new Set(['steam-strict']), rulesets: new Set(['steam-strict']),
}, { }, {
key: 'tanks_ignore_button_while_moving', key: 'no_backwards_override',
label: "Blue tanks ignore blue buttons while moving", label: "Players can't override backwards on a force floor",
rulesets: new Set(['lynx']), rulesets: new Set(['lynx']),
}, { }, {
key: 'blobs_use_tw_prng', key: 'traps_like_lynx',
label: "Blobs use the Tile World RNG", label: "Traps eject faster, and eject when already open",
rulesets: new Set(['lynx']), rulesets: new Set(['lynx']),
}, { }, {
key: 'teeth_target_internal_position', key: 'blue_floors_vanish_on_arrive',
label: "Teeth target the player's internal position", label: "Fake blue walls vanish when stepped on",
rulesets: new Set(['lynx']), rulesets: new Set(['lynx']),
}, { }, {
key: 'rff_blocks_monsters', key: 'popwalls_pop_on_arrive',
label: "Random force floors block monsters", label: "Recessed walls activate when stepped on",
rulesets: new Set(['ms']),
}, {
key: 'bonking_isnt_instant',
label: "Bonking while sliding doesn't apply instantly",
rulesets: new Set(['lynx', 'ms']), rulesets: new Set(['lynx', 'ms']),
}, { }, {
key: 'fire_allows_most_monsters', key: 'rff_actually_random',
label: "Fire doesn't block monsters, except bugs and walkers", label: "Random force floors are actually random",
rulesets: new Set(['ms']), rulesets: new Set(['ms']),
}, }],
]; }, {
title: "Items",
flags: [{
key: 'cloned_bowling_balls_can_be_lost',
label: "Bowling balls on cloners are destroyed when fired at point blank",
rulesets: new Set(['steam-strict']),
}, {
key: 'bombs_immediately_detonate_under_players',
label: "Mines under players detonate when the level starts",
rulesets: new Set(['steam-strict']),
}, {
key: 'bombs_detonate_on_arrive',
label: "Mines detonate only when stepped on",
rulesets: new Set(['lynx', 'ms']),
}, {
key: 'monsters_ignore_keys',
label: "Monsters completely ignore keys",
rulesets: new Set(['ms']),
}],
}];
export function compat_flags_for_ruleset(ruleset) { export function compat_flags_for_ruleset(ruleset) {
let compat = {}; let compat = {};
for (let compatdef of COMPAT_FLAGS) { for (let category of COMPAT_FLAG_CATEGORIES) {
for (let compatdef of category.flags) {
if (compatdef.rulesets.has(ruleset)) { if (compatdef.rulesets.has(ruleset)) {
compat[compatdef.key] = true; compat[compatdef.key] = true;
} }
} }
}
return compat; return compat;
} }

View File

@ -2,7 +2,7 @@
// - steam: if a player character starts on a force floor they won't be able to make any voluntary movements until they are no longer on a force floor // - steam: if a player character starts on a force floor they won't be able to make any voluntary movements until they are no longer on a force floor
import * as fflate from './vendor/fflate.js'; import * as fflate from './vendor/fflate.js';
import { COMPAT_FLAGS, COMPAT_RULESET_LABELS, COMPAT_RULESET_ORDER, INPUT_BITS, TICS_PER_SECOND, compat_flags_for_ruleset } from './defs.js'; import { COMPAT_FLAG_CATEGORIES, COMPAT_RULESET_LABELS, COMPAT_RULESET_ORDER, INPUT_BITS, TICS_PER_SECOND, compat_flags_for_ruleset } from './defs.js';
import * as c2g from './format-c2g.js'; import * as c2g from './format-c2g.js';
import * as dat from './format-dat.js'; import * as dat from './format-dat.js';
import * as format_base from './format-base.js'; import * as format_base from './format-base.js';
@ -3301,8 +3301,8 @@ class CompatOverlay extends DialogOverlay {
"These are more technical settings, and as such are documented in full on ", "These are more technical settings, and as such are documented in full on ",
mk('a', {href: 'https://github.com/eevee/lexys-labyrinth/wiki/Compatibility'}, "the project wiki"), mk('a', {href: 'https://github.com/eevee/lexys-labyrinth/wiki/Compatibility'}, "the project wiki"),
"."), "."),
mk('p', "The short version is: Lexy mode is fine 99% of the time. If a level doesn't seem to work, try the mode for the game it's designed for. Microsoft mode is best-effort and nothing is guaranteed."), mk('p', "Lexy mode should be fine 99% of the time. If a level doesn't seem to work, try the mode for the game it's designed for."),
mk('p', "Changes won't take effect until you restart the level or change levels."), mk('p', "Changes take effect when a level starts."),
); );
let button_set = mk('div.radio-faux-button-set'); let button_set = mk('div.radio-faux-button-set');
@ -3321,7 +3321,7 @@ class CompatOverlay extends DialogOverlay {
if (ruleset === 'custom') if (ruleset === 'custom')
return; return;
for (let compat of COMPAT_FLAGS) { for (let compat of this.all_compat_flags) {
this.set(compat.key, compat.rulesets.has(ruleset)); this.set(compat.key, compat.rulesets.has(ruleset));
} }
}); });
@ -3329,7 +3329,12 @@ class CompatOverlay extends DialogOverlay {
// TODO include the section dividers, somehow // TODO include the section dividers, somehow
let list = mk('ul.compat-flags'); let list = mk('ul.compat-flags');
for (let compat of COMPAT_FLAGS) { this.all_compat_flags = [];
for (let category of COMPAT_FLAG_CATEGORIES) {
this.all_compat_flags.push(...category.flags);
list.append(mk('h2', category.title));
for (let compat of category.flags) {
let label = mk('label', let label = mk('label',
mk('input', {type: 'checkbox', name: compat.key}), mk('input', {type: 'checkbox', name: compat.key}),
mk('span.-desc', compat.label), mk('span.-desc', compat.label),
@ -3347,12 +3352,13 @@ class CompatOverlay extends DialogOverlay {
} }
list.append(mk('li', label)); list.append(mk('li', label));
} }
}
list.addEventListener('change', ev => { list.addEventListener('change', ev => {
// If the current set of flags exactly matches one of the presets, highlight that button // If the current set of flags exactly matches one of the presets, highlight that button
let selected_ruleset = 'custom'; let selected_ruleset = 'custom';
for (let ruleset of COMPAT_RULESET_ORDER) { for (let ruleset of COMPAT_RULESET_ORDER) {
let ok = true; let ok = true;
for (let compat of COMPAT_FLAGS) { for (let compat of this.all_compat_flags) {
if (this.root.elements[compat.key].checked !== compat.rulesets.has(ruleset)) { if (this.root.elements[compat.key].checked !== compat.rulesets.has(ruleset)) {
ok = false; ok = false;
break; break;
@ -3372,7 +3378,7 @@ class CompatOverlay extends DialogOverlay {
// Populate everything to match the current settings // Populate everything to match the current settings
this.root.elements['__ruleset__'].value = this.conductor._compat_ruleset ?? 'custom'; this.root.elements['__ruleset__'].value = this.conductor._compat_ruleset ?? 'custom';
for (let compat of COMPAT_FLAGS) { for (let compat of this.all_compat_flags) {
this.set(compat.key, !! this.conductor.compat[compat.key]); this.set(compat.key, !! this.conductor.compat[compat.key]);
} }
@ -3397,7 +3403,7 @@ class CompatOverlay extends DialogOverlay {
save(permanent) { save(permanent) {
let flags = {}; let flags = {};
for (let compat of COMPAT_FLAGS) { for (let compat of this.all_compat_flags) {
if (this.root.elements[compat.key].checked) { if (this.root.elements[compat.key].checked) {
flags[compat.key] = true; flags[compat.key] = true;
} }
@ -3915,7 +3921,6 @@ class Conductor {
document.querySelector('#main-compat').addEventListener('click', () => { document.querySelector('#main-compat').addEventListener('click', () => {
new CompatOverlay(this).open(); new CompatOverlay(this).open();
}); });
document.querySelector('#main-compat output').textContent = COMPAT_RULESET_LABELS[this._compat_ruleset ?? 'custom'];
// Bind to the navigation headers, which list the current level pack // Bind to the navigation headers, which list the current level pack
// and level // and level
@ -4266,6 +4271,7 @@ class Conductor {
this._compat_ruleset = ruleset; this._compat_ruleset = ruleset;
} }
document.querySelector('#main-compat img').src = `icons/compat-${ruleset}.png`;
document.querySelector('#main-compat output').textContent = COMPAT_RULESET_LABELS[ruleset]; document.querySelector('#main-compat output').textContent = COMPAT_RULESET_LABELS[ruleset];
this.compat = flags; this.compat = flags;

View File

@ -327,6 +327,16 @@ button.--pressed,
.dialog a:visited { .dialog a:visited {
color: hsl(255, 50%, 50%); color: hsl(255, 50%, 50%);
} }
.dialog code {
color: hsl(var(--main-hue), 50%, 30%);
}
.dialog h2 {
color: hsl(var(--main-hue), 75%, 25%);
border-bottom: 1px dotted hsl(var(--main-hue), 50%, 40%);
}
.dialog h2:nth-child(n+1) {
margin-top: 1rem;
}
dl.formgrid { dl.formgrid {
display: grid; display: grid;
grid: auto-flow min-content / 1fr 4fr; grid: auto-flow min-content / 1fr 4fr;
@ -525,6 +535,9 @@ ul.compat-flags > li > label {
align-items: center; align-items: center;
gap: 0.25em; gap: 0.25em;
} }
ul.compat-flags > li > label > input[type=check] {
margin: 0.25em;
}
ul.compat-flags > li > label > span.-desc { ul.compat-flags > li > label > span.-desc {
flex: 1; flex: 1;
} }
@ -727,6 +740,11 @@ pre.stack-trace {
image-rendering: crisp-edges; image-rendering: crisp-edges;
image-rendering: pixelated; image-rendering: pixelated;
} }
#main-compat > img {
height: 16px;
vertical-align: middle;
margin-right: 0.25em;
}
@media (orientation: portrait) and (max-width: 800px), (orientation: landscape) and (max-height: 600px) { @media (orientation: portrait) and (max-width: 800px), (orientation: landscape) and (max-height: 600px) {
body > header { body > header {