Shim around several compat issues that affect CCLP levels
- CCLP1 #81 requires pushing blocks off of blue walls, which is impossible in CC2 but allowed in TW Lynx (unclear if this is a lynx behavior or a tw bug) - CCLP1 #89 has a tank start on a recessed wall and drive off of it, expecting the recessed wall to be left alone, but under CC2 rules it becomes a wall; such walls are now automatically converted to a new tile, the "doubly recessed wall", which restores the expected behavior without changing how recessed walls work in general - CCLP4 #135 expects pressing a blue button to not affect blue tanks that are currently in mid-slide In addition, the behavior of blue buttons now matches the Lynx/Steam behavior: the press is stored as a flag and queued until the tank is next able to move.
This commit is contained in:
parent
8326b42bc7
commit
9b873764fb
@ -122,6 +122,7 @@ const TILE_ENCODING = {
|
||||
function parse_level(buf, number) {
|
||||
let level = new util.StoredLevel(number);
|
||||
level.has_custom_connections = true;
|
||||
level.use_ccl_compat = true;
|
||||
// Map size is always fixed as 32x32 in CC1
|
||||
level.size_x = 32;
|
||||
level.size_y = 32;
|
||||
|
||||
@ -7,6 +7,7 @@ export class StoredCell extends Array {
|
||||
|
||||
export class StoredLevel {
|
||||
constructor(number) {
|
||||
// TODO still not sure this belongs here
|
||||
this.number = number; // one-based
|
||||
this.title = '';
|
||||
this.password = null;
|
||||
@ -16,6 +17,7 @@ export class StoredLevel {
|
||||
this.viewport_size = 9;
|
||||
this.extra_chunks = [];
|
||||
this.use_cc1_boots = false;
|
||||
this.use_ccl_compat = false;
|
||||
|
||||
this.size_x = 0;
|
||||
this.size_y = 0;
|
||||
|
||||
19
js/game.js
19
js/game.js
@ -162,7 +162,7 @@ export class Level {
|
||||
}
|
||||
|
||||
restart(compat) {
|
||||
this.compat = {};
|
||||
this.compat = compat;
|
||||
|
||||
// playing: normal play
|
||||
// success: has been won
|
||||
@ -425,9 +425,13 @@ export class Level {
|
||||
}
|
||||
|
||||
let direction_preference;
|
||||
// Actors can't make voluntary moves on ice, so they're stuck with
|
||||
// whatever they've got
|
||||
if (this.compat.sliding_tanks_ignore_button &&
|
||||
actor.slide_mode && actor.pending_reverse)
|
||||
{
|
||||
this._set_prop(actor, 'pending_reverse', false);
|
||||
}
|
||||
if (actor.slide_mode === 'ice') {
|
||||
// Actors can't make voluntary moves on ice; they just slide
|
||||
direction_preference = [actor.direction];
|
||||
}
|
||||
else if (actor.slide_mode === 'force') {
|
||||
@ -458,8 +462,13 @@ export class Level {
|
||||
}
|
||||
}
|
||||
else if (actor.type.movement_mode === 'forward') {
|
||||
// blue tank behavior: keep moving forward
|
||||
direction_preference = [actor.direction];
|
||||
// blue tank behavior: keep moving forward, reverse if the flag is set
|
||||
let direction = actor.direction;
|
||||
if (actor.pending_reverse) {
|
||||
direction = DIRECTIONS[actor.direction].opposite;
|
||||
this._set_prop(actor, 'pending_reverse', false);
|
||||
}
|
||||
direction_preference = [direction];
|
||||
}
|
||||
else if (actor.type.movement_mode === 'follow-left') {
|
||||
// bug behavior: always try turning as left as possible, and
|
||||
|
||||
56
js/main.js
56
js/main.js
@ -248,7 +248,12 @@ class Player extends PrimaryView {
|
||||
this.scale = 1;
|
||||
|
||||
this.compat = {
|
||||
popwalls_react_on_arrive: false,
|
||||
auto_convert_ccl_popwalls: true,
|
||||
blue_walls_walkable: true,
|
||||
sliding_tanks_ignore_button: true,
|
||||
tiles_react_instantly: false,
|
||||
allow_flick: false,
|
||||
};
|
||||
|
||||
this.root.style.setProperty('--tile-width', `${this.conductor.tileset.size_x}px`);
|
||||
@ -607,7 +612,7 @@ class Player extends PrimaryView {
|
||||
}
|
||||
this.conductor.save_savefile();
|
||||
|
||||
this.level = new Level(stored_level, this.compat);
|
||||
this.level = new Level(stored_level, this.gather_compat_options(stored_level));
|
||||
this.level.sfx = this.sfx_player;
|
||||
this.renderer.set_level(this.level);
|
||||
this.root.classList.toggle('--has-demo', !!this.level.stored_level.demo);
|
||||
@ -617,10 +622,20 @@ class Player extends PrimaryView {
|
||||
}
|
||||
|
||||
restart_level() {
|
||||
this.level.restart(this.compat);
|
||||
this.level.restart(this.gather_compat_options(this.level.stored_level));
|
||||
this._clear_state();
|
||||
}
|
||||
|
||||
gather_compat_options(stored_level) {
|
||||
let ret = {};
|
||||
if (stored_level.use_ccl_compat) {
|
||||
for (let [key, value] of Object.entries(this.compat)) {
|
||||
ret[key] = value;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Call after loading or restarting a level
|
||||
_clear_state() {
|
||||
this.set_state('waiting');
|
||||
@ -1374,15 +1389,40 @@ const AESTHETIC_OPTIONS = [{
|
||||
note: "Chip's Challenge typically draws everything in a grid, which looks a bit funny for tall skinny objects like... the player. And teeth. This option draws both of them raised up slightly, so they'll break the grid and add a slight 3D effect. May not work for all tilesets.",
|
||||
}];
|
||||
const COMPAT_OPTIONS = [{
|
||||
key: 'popwalls_react_on_arrive',
|
||||
label: "Recessed walls trigger when stepped on",
|
||||
impls: ['lynx', 'ms'],
|
||||
note: "This was the behavior in both versions of CC1, but CC2 changed them to trigger when stepped off of (probably to match the behavior of turtles). Some CCLP levels depend on the old behavior. See the next option for a more conservative solution.",
|
||||
}, {
|
||||
key: 'auto_convert_ccl_popwalls',
|
||||
label: "Fix loaded recessed walls",
|
||||
impls: ['lynx', 'ms'],
|
||||
note: "This is a more conservative solution to the problem with recessed walls. It replaces recessed walls with a new tile, \"doubly recessed walls\", only if they begin the level with something on top of them. This should resolve compatibility issues without changing the behavior of recessed walls.",
|
||||
}, {
|
||||
key: 'blue_walls_walkable',
|
||||
label: "Blocks can be pushed off of blue walls",
|
||||
impls: ['lynx'],
|
||||
note: "Generally, you can only push a block if it's in a space you could otherwise move into, but Tile World Lynx allows pushing blocks off of blue walls. (Unclear whether this is a Tile World bug, or a Lynx bug that Tile World is replicating.) The same effect can be achieved in Steam by using a recessed wall instead, assuming you're using the Steam recessed wall behavior.",
|
||||
}, {
|
||||
key: 'sliding_tanks_ignore_button',
|
||||
label: "Blue tanks ignore blue buttons while sliding",
|
||||
impls: ['lynx'],
|
||||
note: "In Lynx, due to what is almost certainly a bug, blue tanks would simply not react at all if a blue button were pressed while they were in mid-movement. Steam fixed this, but it also made blue tanks \"remember\" a button press if they were in the middle of a slide and then turn around once they were finished, and this subtle change broke at least one CCLP level. (There is no compat option for ignoring a button press while moving normally, as that makes the game worse for no known benefit.)",
|
||||
}, {
|
||||
key: 'tiles_react_instantly',
|
||||
label: "Tiles react instantly",
|
||||
impls: ['lynx', 'ms'],
|
||||
note: "In classic CC, actors moved instantly from one tile to another, so tiles would react (e.g., buttons would become pressed) instantly as well. CC2 made actors slide smoothly between tiles, and it made more sense visually for the reactions to only happen once the sliding animation had finished. That's technically a gameplay change, since it delays a lot of tile behavior for 4 tics (the time it takes most actors to move), so here's a compat option. Works best in conjunction with disabling smooth scrolling; otherwise you'll see strange behavior like completing a level before actually stepping onto the exit.",
|
||||
impls: ['ms'],
|
||||
note: "CC originally had objects slide smoothly from one tile to another, so most tiles only responded when the movement completed. In the Microsoft port, though, everything moves instantly (and then waits before moving again), so tiles respond right away.",
|
||||
}, {
|
||||
key: 'allow_flick',
|
||||
label: "Allow flicking",
|
||||
impls: ['ms'],
|
||||
note: "Generally, you can only push a block if it's in a space you could otherwise move into. Due to a bug, the Microsoft port allows pushing blocks that are on top of walls, thin walls, ice corners, etc., and this maneuver is called a \"flick\".",
|
||||
}];
|
||||
const COMPAT_IMPLS = {
|
||||
lynx: "Lynx, the original version",
|
||||
ms: "Microsoft's Windows port",
|
||||
cc2bug: "Bug present in CC2",
|
||||
steam: "The canonical Steam version, but off by default because it's considered a bug",
|
||||
};
|
||||
const OPTIONS_TABS = [{
|
||||
name: 'aesthetic',
|
||||
@ -1430,8 +1470,10 @@ class OptionsOverlay extends DialogOverlay {
|
||||
|
||||
// Compat tab
|
||||
this.tab_blocks['compat'].append(
|
||||
mk('p', "If you don't know what any of these are for, you can pretty safely ignore them."),
|
||||
mk('p', "Changes won't take effect until you restart the level."),
|
||||
mk('p', "Revert to:", mk('button', "Default"), mk('button', "Lynx"), mk('button', "Microsoft"), mk('button', "Steam")),
|
||||
mk('p', "These settings are for compatibility with player-created levels, which sometimes relied on subtle details of the Microsoft or Lynx games and no longer work with the now-canonical Steam rules. The default is to follow the Steam rules as closely as possible (except for bugs), but make a few small tweaks to keep CCL-format levels working."),
|
||||
mk('p', "Changes won't take effect until you restart the level or change levels."),
|
||||
mk('p', "Please note that Microsoft had a number of subtle but complex bugs that Lexy's Labyrinth cannot ever reasonably emulate. The Microsoft settings here are best-effort and not intended to be 100% compatible."),
|
||||
);
|
||||
this._add_options(this.tab_blocks['compat'], COMPAT_OPTIONS);
|
||||
}
|
||||
|
||||
@ -197,6 +197,7 @@ export const CC2_TILESET_LAYOUT = {
|
||||
wired: [[4, 10], [5, 10], [6, 10], [7, 10]],
|
||||
},
|
||||
popwall: [8, 10],
|
||||
popwall2: [8, 10],
|
||||
gravel: [9, 10],
|
||||
ball: [[10, 10], [11, 10], [12, 10], [13, 10], [14, 10]],
|
||||
steel: [15, 10],
|
||||
@ -478,6 +479,7 @@ export const TILE_WORLD_TILESET_LAYOUT = {
|
||||
wall_appearing: [2, 12],
|
||||
gravel: [2, 13],
|
||||
popwall: [2, 14],
|
||||
popwall2: [2, 14],
|
||||
hint: [2, 15],
|
||||
|
||||
thinwall_se: [3, 0],
|
||||
@ -591,6 +593,7 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
|
||||
teeth: Object.assign({}, CC2_TILESET_LAYOUT.teeth, {
|
||||
north: [[0, 32], [1, 32], [2, 32], [1, 32]],
|
||||
}),
|
||||
popwall2: [9, 32],
|
||||
|
||||
// Extra player sprites
|
||||
player: Object.assign({}, CC2_TILESET_LAYOUT.player, {
|
||||
|
||||
@ -107,10 +107,29 @@ const TILE_TYPES = {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
blocks_monsters: true,
|
||||
blocks_blocks: true,
|
||||
on_ready(me, level) {
|
||||
if (level.compat.auto_convert_ccl_popwalls &&
|
||||
me.cell.some(tile => tile.type.is_actor))
|
||||
{
|
||||
// Fix blocks and other actors on top of popwalls by turning them into double
|
||||
// popwalls, which preserves CC2 popwall behavior
|
||||
me.type = TILE_TYPES['popwall2'];
|
||||
}
|
||||
},
|
||||
on_depart(me, level, other) {
|
||||
level.transmute_tile(me, 'wall');
|
||||
},
|
||||
},
|
||||
// LL specific tile that can only be stepped on /twice/, originally used to repair differences
|
||||
// with popwall behavior between Lynx and Steam
|
||||
popwall2: {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
blocks_monsters: true,
|
||||
blocks_blocks: true,
|
||||
on_depart(me, level, other) {
|
||||
level.transmute_tile(me, 'popwall');
|
||||
},
|
||||
},
|
||||
// FIXME in a cc1 tileset, these tiles are opaque >:S
|
||||
thinwall_n: {
|
||||
draw_layer: LAYER_OVERLAY,
|
||||
@ -134,7 +153,16 @@ const TILE_TYPES = {
|
||||
},
|
||||
fake_wall: {
|
||||
draw_layer: LAYER_TERRAIN,
|
||||
blocks_all: true,
|
||||
blocks_monsters: true,
|
||||
blocks_blocks: true,
|
||||
blocks(me, level, other) {
|
||||
if (other.type.is_player && level.compat.blue_walls_walkable) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
on_bump(me, level, other) {
|
||||
if (other.type.can_reveal_walls) {
|
||||
level.transmute_tile(me, 'wall');
|
||||
@ -783,7 +811,7 @@ const TILE_TYPES = {
|
||||
for (let actor of level.actors) {
|
||||
// TODO generify somehow??
|
||||
if (actor.type.name === 'tank_blue') {
|
||||
level.set_actor_direction(actor, DIRECTIONS[actor.direction].opposite);
|
||||
level._set_prop(actor, 'pending_reverse', ! actor.pending_reverse);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
BIN
tileset-lexy.png
BIN
tileset-lexy.png
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Binary file not shown.
Loading…
Reference in New Issue
Block a user