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>
<p>— an <a href="https://github.com/eevee/lexys-labyrinth">open source</a> game by <a href="https://eev.ee/">eevee</a></p>
<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>
</nav>
</header>

View File

@ -135,132 +135,126 @@ export const COMPAT_RULESET_LABELS = {
};
export const COMPAT_RULESET_ORDER = ['lexy', 'steam', 'steam-strict', 'lynx', 'ms', 'custom'];
// FIXME some of the names of the flags themselves kinda suck
export const COMPAT_FLAGS = [
// Level loading
// TODO? /strictly/ speaking, these should be turned on for lynx+ms/lynx respectively, but then i'd
// have to also alter the behavior of the corresponding terrain, which seems kind of silly
{
// TODO some ms compat things that wouldn't be too hard to add:
// - walkers choose a random /unblocked/ direction, not just a random direction
// - (boosting) player cooldown is /zero/ after ending a slide
// - 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',
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']),
}, {
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']),
},
// Core
{
key: 'allow_double_cooldowns',
label: "Actors may cooldown twice in one tic",
rulesets: new Set(['steam', 'steam-strict', 'lynx']),
}],
}, {
title: "Actor behavior",
flags: [{
key: 'emulate_60fps',
label: "Actors update at 60 FPS",
rulesets: new Set(['steam', 'steam-strict']),
}, {
key: 'no_separate_idle_phase',
label: "Actors teleport immediately after moving",
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',
label: "Players always move last",
rulesets: new Set(['lynx', 'ms']),
label: "Players always update last",
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',
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']),
}, {
// Note that this requires no_early_push as well
key: 'player_safe_at_decision_time',
label: "Players can't be trampled at decision time",
rulesets: new Set(['lynx']),
rulesets: new Set(['lynx', 'ms']),
}, {
key: 'emulate_60fps',
label: "Game runs at 60 FPS",
rulesets: new Set(['steam', 'steam-strict']),
}, {
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: 'bonking_isnt_instant',
label: "Bonking while sliding doesn't apply instantly",
rulesets: new Set(['lynx', 'ms']),
}, {
key: 'actors_move_instantly',
label: "Movement happens instantly",
rulesets: new Set(['ms']),
},
// Tiles
{
key: 'rff_actually_random',
label: "Random force floors are actually random",
label: "Movement is instant",
rulesets: new Set(['ms']),
}],
}, {
key: 'no_backwards_override',
label: "Players can't override backwards on a force floor",
rulesets: new Set(['lynx']),
}, {
key: 'traps_like_lynx',
label: "Traps eject faster, and even when already open",
rulesets: new Set(['lynx']),
}, {
key: 'popwalls_pop_on_arrive',
label: "Recessed walls activate when stepped on",
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",
title: "Monsters",
flags: [{
// 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
// TODO? in lynx they ignore the button while in motion too
// TODO what about in a trap, in every game??
// 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 obey blue buttons even on clone machines",
rulesets: new Set(['steam-strict']),
}, {
key: 'cloned_bowling_balls_can_be_lost',
label: "Bowling balls on cloners are destroyed when fired at point blank",
key: 'tanks_ignore_button_while_moving',
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']),
}, {
key: 'monsters_ignore_keys',
label: "Monsters completely ignore keys",
rulesets: new Set(['ms']),
},
// Blocks
{
key: 'no_early_push',
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?
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',
label: "Ice blocks emulate pgchip rules",
label: "Ice blocks use pgchip rules",
rulesets: new Set(['ms']),
}, {
key: 'allow_pushing_blocks_off_faux_walls',
label: "Blocks may be pushed off of blue (fake), invisible, and revealing walls",
rulesets: new Set(['lynx']),
}, {
key: 'emulate_spring_mining',
label: "Spring mining is possible",
rulesets: new Set(['steam-strict']),
}, {
key: 'block_splashes_dont_block',
label: "Block splashes don't block the player",
@ -271,50 +265,64 @@ export const COMPAT_FLAGS = [
label: "Flicking is possible",
rulesets: new Set(['ms']),
*/
},
// Monsters
{
// TODO? in lynx they ignore the button while in motion too
// TODO what about in a trap, in every game??
// 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",
}],
}, {
title: "Terrain",
flags: [{
key: 'green_teleports_can_fail',
label: "Green teleporters sometimes fail",
rulesets: new Set(['steam-strict']),
}, {
key: 'tanks_ignore_button_while_moving',
label: "Blue tanks ignore blue buttons while moving",
key: 'no_backwards_override',
label: "Players can't override backwards on a force floor",
rulesets: new Set(['lynx']),
}, {
key: 'blobs_use_tw_prng',
label: "Blobs use the Tile World RNG",
key: 'traps_like_lynx',
label: "Traps eject faster, and eject when already open",
rulesets: new Set(['lynx']),
}, {
key: 'teeth_target_internal_position',
label: "Teeth target the player's internal position",
key: 'blue_floors_vanish_on_arrive',
label: "Fake blue walls vanish when stepped on",
rulesets: new Set(['lynx']),
}, {
key: 'rff_blocks_monsters',
label: "Random force floors block monsters",
rulesets: new Set(['ms']),
}, {
key: 'bonking_isnt_instant',
label: "Bonking while sliding doesn't apply instantly",
key: 'popwalls_pop_on_arrive',
label: "Recessed walls activate when stepped on",
rulesets: new Set(['lynx', 'ms']),
}, {
key: 'fire_allows_most_monsters',
label: "Fire doesn't block monsters, except bugs and walkers",
key: 'rff_actually_random',
label: "Random force floors are actually random",
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) {
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)) {
compat[compatdef.key] = true;
}
}
}
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
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 dat from './format-dat.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 ",
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', "Changes won't take effect until you restart the level or change levels."),
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 take effect when a level starts."),
);
let button_set = mk('div.radio-faux-button-set');
@ -3321,7 +3321,7 @@ class CompatOverlay extends DialogOverlay {
if (ruleset === 'custom')
return;
for (let compat of COMPAT_FLAGS) {
for (let compat of this.all_compat_flags) {
this.set(compat.key, compat.rulesets.has(ruleset));
}
});
@ -3329,7 +3329,12 @@ class CompatOverlay extends DialogOverlay {
// TODO include the section dividers, somehow
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',
mk('input', {type: 'checkbox', name: compat.key}),
mk('span.-desc', compat.label),
@ -3347,12 +3352,13 @@ class CompatOverlay extends DialogOverlay {
}
list.append(mk('li', label));
}
}
list.addEventListener('change', ev => {
// If the current set of flags exactly matches one of the presets, highlight that button
let selected_ruleset = 'custom';
for (let ruleset of COMPAT_RULESET_ORDER) {
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)) {
ok = false;
break;
@ -3372,7 +3378,7 @@ class CompatOverlay extends DialogOverlay {
// Populate everything to match the current settings
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]);
}
@ -3397,7 +3403,7 @@ class CompatOverlay extends DialogOverlay {
save(permanent) {
let flags = {};
for (let compat of COMPAT_FLAGS) {
for (let compat of this.all_compat_flags) {
if (this.root.elements[compat.key].checked) {
flags[compat.key] = true;
}
@ -3915,7 +3921,6 @@ class Conductor {
document.querySelector('#main-compat').addEventListener('click', () => {
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
// and level
@ -4266,6 +4271,7 @@ class Conductor {
this._compat_ruleset = ruleset;
}
document.querySelector('#main-compat img').src = `icons/compat-${ruleset}.png`;
document.querySelector('#main-compat output').textContent = COMPAT_RULESET_LABELS[ruleset];
this.compat = flags;

View File

@ -327,6 +327,16 @@ button.--pressed,
.dialog a:visited {
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 {
display: grid;
grid: auto-flow min-content / 1fr 4fr;
@ -525,6 +535,9 @@ ul.compat-flags > li > label {
align-items: center;
gap: 0.25em;
}
ul.compat-flags > li > label > input[type=check] {
margin: 0.25em;
}
ul.compat-flags > li > label > span.-desc {
flex: 1;
}
@ -727,6 +740,11 @@ pre.stack-trace {
image-rendering: crisp-edges;
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) {
body > header {