Add obituaries, and get them out of the game itself

This commit is contained in:
Eevee (Evelyn Woods) 2020-09-16 19:21:32 -06:00
parent fe12c599bc
commit 57810da581
6 changed files with 182 additions and 58 deletions

View File

@ -52,7 +52,11 @@
</main> </main>
<main id="player" hidden> <main id="player" hidden>
<div class="level"><!-- level canvas and any overlays go here --></div> <div class="level"><!-- level canvas and any overlays go here --></div>
<div class="bummer"></div> <div class="overlay-message">
<h1 class="-top"></h1>
<div class="-middle"></div>
<p class="-bottom"></p>
</div>
<div class="message"></div> <div class="message"></div>
<div class="chips"> <div class="chips">
<h3>Chips</h3> <h3>Chips</h3>

View File

@ -622,7 +622,7 @@ export class Level {
this.time_remaining -= 1; this.time_remaining -= 1;
if (this.time_remaining <= 0) { if (this.time_remaining <= 0) {
this.fail("Time's up!"); this.fail('time');
} }
} }
else { else {
@ -767,31 +767,28 @@ export class Level {
} }
// Check for a couple effects that always apply immediately // Check for a couple effects that always apply immediately
// TODO i guess this covers blocks too
// TODO do blocks smash monsters? // TODO do blocks smash monsters?
for (let tile of goal_cell) { for (let tile of goal_cell) {
if (actor.type.is_player && tile.type.is_monster) {
this.fail(tile.type.name);
}
else if (actor.type.is_monster && tile.type.is_player) {
this.fail(actor.type.name);
}
else if (actor.type.is_block && tile.type.is_player) {
this.fail('squished');
}
if (tile.type.slide_mode && ! actor.ignores(tile.type.name)) { if (tile.type.slide_mode && ! actor.ignores(tile.type.name)) {
this.make_slide(actor, tile.type.slide_mode); this.make_slide(actor, tile.type.slide_mode);
} }
if ((actor.type.is_player && tile.type.is_monster) ||
(actor.type.is_monster && tile.type.is_player))
{
// TODO ooh, obituaries
this.fail("Oops! Watch out for creatures!");
return;
}
if (actor.type.is_block && tile.type.is_player) {
// TODO ooh, obituaries
this.fail("squish");
return;
}
} }
// If we're stepping directly on the player, that kills them too // If we're stepping directly on the player, that kills them too
// TODO this only works because i have the player move first; in lynx the check is the other // TODO this only works because i have the player move first; in lynx the check is the other
// way around // way around
if (actor.type.is_monster && goal_cell === this.player_leaving_cell) { if (actor.type.is_monster && goal_cell === this.player_leaving_cell) {
this.fail("Oops! Watch out for creatures!"); this.fail(actor.type.name);
} }
if (this.compat.tiles_react_instantly) { if (this.compat.tiles_react_instantly) {
@ -931,7 +928,7 @@ export class Level {
this.time_remaining = 1; this.time_remaining = 1;
} }
else { else {
this.fail("Time's up!"); this.fail('time');
} }
} }
} }
@ -939,10 +936,10 @@ export class Level {
fail(message) { fail(message) {
this.pending_undo.push(() => { this.pending_undo.push(() => {
this.state = 'playing'; this.state = 'playing';
this.fail_message = null; this.fail_reason = null;
}); });
this.state = 'failure'; this.state = 'failure';
this.fail_message = message; this.fail_reason = message;
throw new GameEnded; throw new GameEnded;
} }

View File

@ -8,7 +8,7 @@ import { Level } from './game.js';
import CanvasRenderer from './renderer-canvas.js'; import CanvasRenderer from './renderer-canvas.js';
import { Tileset, CC2_TILESET_LAYOUT, TILE_WORLD_TILESET_LAYOUT } from './tileset.js'; import { Tileset, CC2_TILESET_LAYOUT, TILE_WORLD_TILESET_LAYOUT } from './tileset.js';
import TILE_TYPES from './tiletypes.js'; import TILE_TYPES from './tiletypes.js';
import { mk, promise_event, fetch, walk_grid } from './util.js'; import { random_choice, mk, promise_event, fetch, walk_grid } from './util.js';
const PAGE_TITLE = "Lexy's Labyrinth"; const PAGE_TITLE = "Lexy's Labyrinth";
// Stackable modal overlay of some kind, usually a dialog // Stackable modal overlay of some kind, usually a dialog
@ -136,6 +136,65 @@ const ACTION_DIRECTIONS = {
left: 'west', left: 'west',
right: 'east', right: 'east',
}; };
const OBITUARIES = {
drowned: [
"player tried out water cooling",
"player fell into the c",
],
burned: [
"player's core temp got too high",
"player's plans went up in smoke",
],
exploded: [
"player didn't watch where they step",
"player is having a blast",
"player tripped over something of mine",
],
squished: [
"player was overwhelmed by a block of ram",
"player became two-dimensional",
],
time: [
"player tried to overclock",
"player's time ran out",
"player's speedrun went badly",
],
generic: [
"player had a bad time",
],
// Specific creatures
ball: [
"player is having a ball",
],
walker: [
"player let it walk all over them",
],
fireball: [
"player had a meltdown",
],
glider: [
"player's ship came in",
],
tank_blue: [
"player didn't watch where they tread",
],
tank_yellow: [
"player let things get out of control",
],
bug: [
"player has ants in their pants",
],
paramecium: [
"player has the creepy crawlies",
],
teeth: [
"player got a mega bite",
],
blob: [
"player didn't do a gooed job",
],
};
class Player extends PrimaryView { class Player extends PrimaryView {
constructor(conductor) { constructor(conductor) {
super(conductor, document.body.querySelector('main#player')); super(conductor, document.body.querySelector('main#player'));
@ -163,12 +222,12 @@ class Player extends PrimaryView {
this.root.style.setProperty('--tile-width', `${this.conductor.tileset.size_x}px`); this.root.style.setProperty('--tile-width', `${this.conductor.tileset.size_x}px`);
this.root.style.setProperty('--tile-height', `${this.conductor.tileset.size_y}px`); this.root.style.setProperty('--tile-height', `${this.conductor.tileset.size_y}px`);
this.level_el = this.root.querySelector('.level'); this.level_el = this.root.querySelector('.level');
this.overlay_message_el = this.root.querySelector('.overlay-message');
this.message_el = this.root.querySelector('.message'); this.message_el = this.root.querySelector('.message');
this.chips_el = this.root.querySelector('.chips output'); this.chips_el = this.root.querySelector('.chips output');
this.time_el = this.root.querySelector('.time output'); this.time_el = this.root.querySelector('.time output');
this.bonus_el = this.root.querySelector('.bonus output'); this.bonus_el = this.root.querySelector('.bonus output');
this.inventory_el = this.root.querySelector('.inventory'); this.inventory_el = this.root.querySelector('.inventory');
this.bummer_el = this.root.querySelector('.bummer');
this.input_el = this.root.querySelector('.input'); this.input_el = this.root.querySelector('.input');
this.demo_el = this.root.querySelector('.demo'); this.demo_el = this.root.querySelector('.demo');
@ -589,26 +648,67 @@ class Player extends PrimaryView {
this.state = new_state; this.state = new_state;
// Populate the overlay
let overlay_reason = '';
let overlay_top = '';
let overlay_middle = null;
let overlay_bottom = '';
if (this.state === 'waiting') { if (this.state === 'waiting') {
this.bummer_el.textContent = "Ready!"; overlay_reason = 'waiting';
} overlay_middle = "Ready!";
else if (this.state === 'playing' || this.state === 'rewinding') {
this.bummer_el.textContent = "";
} }
else if (this.state === 'paused') { else if (this.state === 'paused') {
this.bummer_el.textContent = "/// paused ///"; overlay_reason = 'paused';
overlay_bottom = "/// paused ///";
} }
else if (this.state === 'stopped') { else if (this.state === 'stopped') {
if (this.level.state === 'failure') { if (this.level.state === 'failure') {
this.bummer_el.textContent = this.level.fail_message; overlay_reason = 'failure';
overlay_top = "whoops";
let obits = OBITUARIES[this.level.fail_reason] ?? OBITUARIES['generic'];
overlay_bottom = random_choice(obits);
} }
else { else {
this.bummer_el.textContent = ""; overlay_reason = 'success';
let base = (this.conductor.level_index + 1) * 500; let base = (this.conductor.level_index + 1) * 500;
let time = Math.ceil((this.level.time_remaining ?? 0) / 20) * 10; let time = Math.ceil((this.level.time_remaining ?? 0) / 20) * 10;
this.bummer_el.append( // Pick a success message
mk('p', "go bit buster!"), // TODO done on first try; took many tries
mk('dl.score-chart', let time_left_fraction = null;
if (this.level.time_remaining !== null && this.level.stored_level.time_limit !== null) {
time_left_fraction = this.level.time_remaining / 20 / this.level.stored_level.time_limit;
}
if (this.level.chips_remaining > 0) {
overlay_top = random_choice([
"socket to em!", "go bug blaster!",
]);
}
else if (this.level.time_remaining && this.level.time_remaining < 200) {
overlay_top = random_choice([
"in the nick of time!", "cutting it close!",
]);
}
else if (time_left_fraction !== null && time_left_fraction > 1) {
overlay_top = random_choice([
"faster than light!", "impossible speed!", "pipelined!",
]);
}
else if (time_left_fraction !== null && time_left_fraction > 0.75) {
overlay_top = random_choice([
"lightning quick!", "nice speedrun!", "eagerly evaluated!",
]);
}
else {
overlay_top = random_choice([
"you did it!", "nice going!", "great job!", "good work!",
"onwards!", "tubular!", "yeehaw!", "hot damn!",
"alphanumeric!", "nice dynamic typing!",
]);
}
overlay_bottom = "press spacebar to continue";
overlay_middle = mk('dl.score-chart',
mk('dt', "base score"), mk('dt', "base score"),
mk('dd', base), mk('dd', base),
mk('dt', "time bonus"), mk('dt', "time bonus"),
@ -621,10 +721,17 @@ class Player extends PrimaryView {
mk('dd', "(TODO)"), mk('dd', "(TODO)"),
mk('dt', "total score"), mk('dt', "total score"),
mk('dd', "(TODO)"), mk('dd', "(TODO)"),
),
); );
} }
} }
this.overlay_message_el.setAttribute('data-reason', overlay_reason);
this.overlay_message_el.querySelector('.-top').textContent = overlay_top;
this.overlay_message_el.querySelector('.-bottom').textContent = overlay_bottom;
let middle = this.overlay_message_el.querySelector('.-middle');
middle.textContent = '';
if (overlay_middle) {
middle.append(overlay_middle);
}
// The advance and redraw methods run in a loop, but they cancel // The advance and redraw methods run in a loop, but they cancel
// themselves if the game isn't running, so restart them here // themselves if the game isn't running, so restart them here

View File

@ -291,7 +291,7 @@ const TILE_TYPES = {
} }
else if (other.type.is_player) { else if (other.type.is_player) {
level.transmute_tile(other, 'player_burned'); level.transmute_tile(other, 'player_burned');
level.fail("Oops! You can't walk on fire without fire boots!"); level.fail('burned');
} }
else { else {
level.remove_tile(other); level.remove_tile(other);
@ -312,7 +312,7 @@ const TILE_TYPES = {
} }
else if (other.type.is_player) { else if (other.type.is_player) {
level.transmute_tile(other, 'splash'); level.transmute_tile(other, 'splash');
level.fail("swimming with the fishes"); level.fail('drowned');
} }
else { else {
level.transmute_tile(other, 'splash'); level.transmute_tile(other, 'splash');
@ -436,7 +436,7 @@ const TILE_TYPES = {
let was_player = other.type.is_player; let was_player = other.type.is_player;
level.transmute_tile(other, 'explosion'); level.transmute_tile(other, 'explosion');
if (was_player) { if (was_player) {
level.fail("watch where you step"); level.fail('exploded');
} }
}, },
}, },
@ -555,7 +555,7 @@ const TILE_TYPES = {
let was_player = other.type.is_player; let was_player = other.type.is_player;
level.transmute_tile(other, 'explosion'); level.transmute_tile(other, 'explosion');
if (was_player) { if (was_player) {
level.fail("watch where you step"); level.fail('exploded');
} }
}, },
}, },

View File

@ -1,3 +1,7 @@
export function random_choice(list) {
return list[Math.floor(Math.random() * list.length)];
}
export function mk(tag_selector, ...children) { export function mk(tag_selector, ...children) {
let [tag, ...classes] = tag_selector.split('.'); let [tag, ...classes] = tag_selector.split('.');
let el = document.createElement(tag); let el = document.createElement(tag);

View File

@ -323,34 +323,46 @@ body[data-mode=player] #editor-play {
--viewport-width: 9; --viewport-width: 9;
--viewport-height: 9; --viewport-height: 9;
} }
.bummer { #player .overlay-message {
grid-area: level; grid-area: level;
place-self: stretch; place-self: stretch;
display: flex; display: grid;
flex-direction: column; grid-template-rows: 1fr 3fr 1fr;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 1; z-index: 1;
font-size: 48px; font-size: calc(0.5 * var(--tile-width) * var(--scale));
padding: 10%; padding: 6.25%;
background: #0009; background: #0009;
color: white; color: white;
text-align: center; text-align: center;
font-weight: bold;
text-shadow: 0 2px 1px black; text-shadow: 0 2px 1px black;
} }
.bummer:empty { #player .overlay-message p {
margin: 0;
}
#player .overlay-message .-top {
font-size: 1.5em;
}
#player .overlay-message .-middle {
}
#player .overlay-message .-bottom {
}
#player .overlay-message[data-reason=""] {
display: none; display: none;
} }
.bummer p { #player .overlay-message[data-reason=failure] {
margin: 0; box-shadow: inset 0 0 calc(4 * var(--tile-width)) var(--tile-width) black;
}
#player .overlay-message[data-reason=success] {
box-shadow: inset 0 0 var(--tile-width) white;
} }
dl.score-chart { dl.score-chart {
display: grid; display: grid;
grid-auto-columns: 1fr 1fr; grid-auto-columns: 1fr 1fr;
font-size: 1.25rem; margin: auto;
font-weight: normal; font-weight: normal;
} }
dl.score-chart dt { dl.score-chart dt {