Add sound effects!

This commit is contained in:
Eevee (Evelyn Woods) 2020-09-25 01:28:31 -06:00
parent 6aee8ed622
commit 40aa845e92
23 changed files with 195 additions and 2 deletions

View File

@ -361,6 +361,8 @@ export class Level {
// TODO but maybe they should be undone anyway so rewind looks better
this.player.is_blocked = false;
this.sfx.set_player_position(this.player.cell);
// First pass: tick cooldowns and animations; have actors arrive in their cells. We do the
// arrival as its own mini pass, for one reason: if the player dies (which will end the game
// immediately), we still want every time's animation to finish, or it'll look like some
@ -591,6 +593,7 @@ export class Level {
// Track whether the player is blocked, for visual effect
if (actor === this.player && p1_primary_direction && ! success) {
this.sfx.play_once('blocked');
actor.is_blocked = true;
}
@ -648,6 +651,9 @@ export class Level {
if (this.time_remaining <= 0) {
this.fail('time');
}
else if (this.time_remaining % 20 === 0 && this.time_remaining < 30 * 20) {
this.sfx.play_once('tick');
}
}
else {
this.pending_undo.push(() => {
@ -831,6 +837,10 @@ export class Level {
this.fail(actor.type.name);
}
if (actor === this.player && goal_cell[0].type.name === 'floor') {
this.sfx.play_once('step-floor');
}
if (this.compat.tiles_react_instantly) {
this.step_on_cell(actor, actor.cell);
}
@ -846,6 +856,12 @@ export class Level {
continue;
if (tile.type.is_item && this.give_actor(actor, tile.type.name)) {
if (tile.type.is_key) {
this.sfx.play_once('get-key', cell);
}
else {
this.sfx.play_once('get-tool', cell);
}
this.remove_tile(tile);
}
else if (tile.type.is_teleporter) {
@ -873,9 +889,13 @@ export class Level {
// XXX not especially undo-efficient
this.remove_tile(actor);
this.add_tile(actor, goal.cell);
if (this.attempt_step(actor, actor.direction))
if (this.attempt_step(actor, actor.direction)) {
// Success, teleportation complete
// Sound plays from the origin cell simply because that's where the sfx player
// thinks the player is currently
this.sfx.play_once('teleport', cell);
break;
}
if (goal === teleporter)
// We've tried every teleporter, including the one they
// stepped on, so leave them on it
@ -936,6 +956,7 @@ export class Level {
collect_chip() {
let current = this.chips_remaining;
if (current > 0) {
this.sfx.play_once('get-chip');
this.pending_undo.push(() => this.chips_remaining = current);
this.chips_remaining--;
}
@ -972,6 +993,13 @@ export class Level {
}
fail(reason) {
if (reason === 'time') {
this.sfx.play_once('timeup');
}
else {
this.sfx.play_once('lose');
}
this.pending_undo.push(() => {
this.state = 'playing';
this.fail_reason = null;
@ -984,6 +1012,7 @@ export class Level {
}
win() {
this.sfx.play_once('win');
this.pending_undo.push(() => this.state = 'playing');
this.state = 'success';
throw new GameEnded;

View File

@ -219,6 +219,126 @@ const OBITUARIES = {
"goo another way next time",
],
};
// Helper class used to let the game play sounds without knowing too much about the Player
class SFXPlayer {
constructor() {
this.ctx = new window.AudioContext;
this.player_x = null;
this.player_y = null;
this.sounds = {};
this.sound_sources = {
// handcrafted
blocked: 'sfx/mmf.ogg',
// https://jummbus.bitbucket.io/#j2N04bombn110s0k0l00e00t3Mm4a3g00j07i0r1O_U00o30T0v0pL0OD0Ou00q1d1f8y0z2C0w2c0h2T2v0kL0OD0Ou02q1d1f6y1z2C1w1b4gp1b0aCTFucgds0
bomb: 'sfx/bomb.ogg',
// https://jummbus.bitbucket.io/#j2N0cbutton-pressn100s0k0l00e00t3Mm1a3g00j07i0r1O_U0o3T0v0pL0OD0Ou00q1d1f3y1z1C2w0c0h0b4p1bJdn51eMUsS0
'button-press': 'sfx/button-press.ogg',
// https://jummbus.bitbucket.io/#j2N0ebutton-releasen100s0k0l00e00t3Mm1a3g00j07i0r1O_U0o3T0v0pL0OD0Ou00q1d1f3y1z1C2w0c0h0b4p1aArdkga4sG0
'button-release': 'sfx/button-release.ogg',
// https://jummbus.bitbucket.io/#j2N04doorn110s0k0l00e00t3Mmfa3g00j07i0r1O_U00o30T0v0zL0OD0Ou00q0d1f8y0z2C0w2c0h0T2v0pL0OD0Ou02q0d1f8y3ziC0w1b4gp1f0aqEQ0lCNzrYUY0
door: 'sfx/door.ogg',
// https://jummbus.bitbucket.io/#j2N08get-chipn100s0k0l00e00t3Mmca3g00j07i0r1O_U0o4T0v0zL0OD0Ou00q1d1f6y1z2C0wac0h0b4p1dFyW7czgUK7aw0
'get-chip': 'sfx/get-chip.ogg',
// https://jummbus.bitbucket.io/#j2N07get-keyn100s0k0l00e00t3Mmfa3g00j07i0r1O_U0o5T0v0pL0OD0Ou00q1d5f8y0z2C0w1c0h0b4p1dFyW85CbwwzBg0
'get-key': 'sfx/get-key.ogg',
// https://jummbus.bitbucket.io/#j2N08get-tooln100s0k0l00e00t3Mm6a3g00j07i0r1O_U0o2T0v0pL0OD0Ou00q1d1f4y2z9C0w2c0h0b4p1bGqKNW4isVk0
'get-tool': 'sfx/get-tool.ogg',
// https://jummbus.bitbucket.io/#j2N06socketn110s0k0l00e00t3Mm4a3g00j07i0r1O_U00o30T5v0pL0OD0Ou05q1d1f8y1z7C1c0h0HU7000U0006000ET2v0pL0OD0Ou02q1d6f5y3z2C0w0b4gp1xGoKHGhFBcn2FyPkxk0rE2AGcNCQyHwUY0
socket: 'sfx/socket.ogg',
// https://jummbus.bitbucket.io/#j2N06splashn110s0k0l00e00t3Mm5a3g00j07i0r1O_U00o20T0v0pL0OD0Ou00q0d0fay0z0C0w9c0h8T2v05L0OD0Ou02q2d6fay0z1C0w0b4gp1lGqKQy02gUY1qh7D1wb2Y0
// https://jummbus.bitbucket.io/#j2N06splashn110s0k0l00e00t3Mm5a3g00j07i0r1O_U00o20T0v0pL0OD0Ou00q0d0fay0z0C0w9c0h8T2v05L0OD0Ou02q2d6fay0z1C0w0b4gp1lGqKQxw_zzM5F4us60IbM0
splash: 'sfx/splash.ogg',
// https://jummbus.bitbucket.io/#j2N0astep-floorn100s0k0l00e00t3Mm6a3g00j07i0r1O_U0o1T0v05L0OD0Ou00q0d2f1y1zjC2w0c0h0b4p1aGaKaxqer00
'step-floor': 'sfx/step-floor.ogg',
// https://jummbus.bitbucket.io/#j2N08teleportn110s1k0l00e00t3Mm7a3g00j07i0r1O_U00o50T0v0pL0OD0Ou00q1d1f8y4z6C2w5c4h0T2v0kL0OD0Ou02q1d7f8y4z3C1w4b4gp1wF2Uzh5wdC18yHH4hhBhHwaATXu0Asds0
teleport: 'sfx/teleport.ogg',
// https://jummbus.bitbucket.io/#j2N05thiefn100s1k0l00e00t3Mm3a3g00j07i0r1O_U0o1T0v0pL0OD0Ou00q1d1f5y1z8C2w2c0h0b4p1fFyUBBr9mGkKKds0
thief: 'sfx/thief.ogg',
// handcrafted
lose: 'sfx/bummer.ogg',
// https://jummbus.bitbucket.io/#j2N04tickn100s0k0l00e00t3Mmca3g00j07i0r1O_U0o2T0v0pL0OD0Ou00q1d1f7y1ziC0w4c0h4b4p1bKqE6Rtxex00
tick: 'sfx/tick.ogg',
// https://jummbus.bitbucket.io/#j2N06timeupn100s0k0l00e00t3Mm4a3g00j07i0r1O_U0o3T1v0pL0OD0Ou01q1d5f4y1z8C1c0A0F0B0V1Q38e0Pa610E0861b4p1dIyfgKPcLucqU0
timeup: 'sfx/timeup.ogg',
// https://jummbus.bitbucket.io/#j2N03winn200s0k0l00e00t2wm9a3g00j07i0r1O_U00o32T0v0EL0OD0Ou00q1d1f5y1z1C2w1c2h0T0v0pL0OD0Ou00q0d1f2y1z2C0w2c3h0b4gp1xFyW4xo31pe0MaCHCbwLbM5cFDgapBOyY0
win: 'sfx/win.ogg',
};
for (let [name, path] of Object.entries(this.sound_sources)) {
this.init_sound(name, path);
}
this.mmf_cooldown = 0;
}
async init_sound(name, path) {
let buf = await fetch(path);
let audiobuf = await this.ctx.decodeAudioData(buf);
this.sounds[name] = {
buf: buf,
audiobuf: audiobuf,
};
}
set_player_position(cell) {
this.player_x = cell.x;
this.player_y = cell.y;
}
play_once(name, cell = null) {
let data = this.sounds[name];
if (! data) {
// Hasn't loaded yet, not much we can do
if (! this.sound_sources[name]) {
console.warn("Tried to play non-existent sound", name);
}
return;
}
// "Mmf" can technically play every tic since bumping into something doesn't give a movement
// cooldown, so give it our own sound cooldown
if (name === 'blocked' && this.player_x !== null) {
if (this.mmf_cooldown > 0) {
return;
}
else {
this.mmf_cooldown = 4;
}
}
let node = this.ctx.createBufferSource();
node.buffer = data.audiobuf;
if (cell && this.player_x !== null) {
// Reduce the volume for further-away sounds
let dx = cell.x - this.player_x;
let dy = cell.y - this.player_y;
let dist = Math.sqrt(dx*dx + dy*dy);
let gain = this.ctx.createGain();
// x/(x + a) is a common and delightful way to get an easy asymptote and output between
// 0 and 1. Here, the result is above 80% for almost everything on screen; drops down
// to 50% for things 20 tiles away (which is, roughly, the periphery when standing in
// the center of a CC1 map), and bottoms out at 12.5% for standing in one corner of a
// CC2 map of max size and hearing something on the far opposite corner.
gain.gain.value = 1 - dist / (dist + 20);
node.connect(gain);
gain.connect(this.ctx.destination);
}
else {
// Play at full volume
node.connect(this.ctx.destination);
}
node.start(this.ctx.currentTime);
}
// Reduce cooldowns
advance_tic() {
if (this.mmf_cooldown > 0) {
this.mmf_cooldown -= 1;
}
}
}
class Player extends PrimaryView {
constructor(conductor) {
super(conductor, document.body.querySelector('main#player'));
@ -473,6 +593,13 @@ class Player extends PrimaryView {
window.addEventListener('resize', ev => {
this.adjust_scale();
});
// TODO yet another thing that should be in setup, but can't be because load_level is called
// first
this.sfx_player = new SFXPlayer;
}
setup() {
}
activate() {
@ -495,6 +622,7 @@ class Player extends PrimaryView {
load_level(stored_level) {
this.level = new Level(stored_level, this.compat);
this.level.sfx = this.sfx_player;
this.renderer.set_level(this.level);
this.root.classList.toggle('--has-demo', !!this.level.stored_level.demo);
// TODO base this on a hash of the UA + some identifier for the pack + the level index. StoredLevel doesn't know its own index atm...
@ -620,6 +748,7 @@ class Player extends PrimaryView {
this.previous_input = input;
this.sfx_player.advance_tic();
this.level.advance_tic(
this.primary_action ? ACTION_DIRECTIONS[this.primary_action] : null,
this.secondary_action ? ACTION_DIRECTIONS[this.secondary_action] : null,

View File

@ -240,7 +240,7 @@ const TILE_TYPES = {
is_swivel: true,
on_depart(me, level, other) {
if (other.direction === 'north') {
level.transmute_tile(me, 'swivel_ne');
level.transmute_tile(me, 'swivel_sw');
}
else if (other.direction === 'west') {
level.transmute_tile(me, 'swivel_ne');
@ -263,6 +263,7 @@ const TILE_TYPES = {
},
on_arrive(me, level, other) {
if (level.take_key_from_actor(other, 'key_red')) {
level.sfx.play_once('door', me.cell);
level.transmute_tile(me, 'floor');
}
},
@ -274,6 +275,7 @@ const TILE_TYPES = {
},
on_arrive(me, level, other) {
if (level.take_key_from_actor(other, 'key_blue')) {
level.sfx.play_once('door', me.cell);
level.transmute_tile(me, 'floor');
}
},
@ -285,6 +287,7 @@ const TILE_TYPES = {
},
on_arrive(me, level, other) {
if (level.take_key_from_actor(other, 'key_yellow')) {
level.sfx.play_once('door', me.cell);
level.transmute_tile(me, 'floor');
}
},
@ -296,6 +299,7 @@ const TILE_TYPES = {
},
on_arrive(me, level, other) {
if (level.take_key_from_actor(other, 'key_green')) {
level.sfx.play_once('door', me.cell);
level.transmute_tile(me, 'floor');
}
},
@ -344,6 +348,7 @@ const TILE_TYPES = {
draw_layer: LAYER_TERRAIN,
on_arrive(me, level, other) {
// TODO cc1 allows items under water, i think; water was on the upper layer
level.sfx.play_once('splash', me.cell);
if (other.type.name === 'dirt_block') {
level.transmute_tile(other, 'splash');
level.transmute_tile(me, 'dirt');
@ -478,6 +483,7 @@ const TILE_TYPES = {
level.fail('exploded');
}
else {
level.sfx.play_once('bomb', me.cell);
level.transmute_tile(other, 'explosion');
}
},
@ -487,6 +493,7 @@ const TILE_TYPES = {
blocks_monsters: true,
blocks_blocks: true,
on_arrive(me, level, other) {
level.sfx.play_once('thief', me.cell);
level.take_all_tools_from_actor(other);
if (other.type.is_player) {
level.adjust_bonus(0, 0.5);
@ -498,6 +505,7 @@ const TILE_TYPES = {
blocks_monsters: true,
blocks_blocks: true,
on_arrive(me, level, other) {
level.sfx.play_once('thief', me.cell);
level.take_all_keys_from_actor(other);
if (other.type.is_player) {
level.adjust_bonus(0, 0.5);
@ -576,6 +584,7 @@ const TILE_TYPES = {
level.fail('exploded');
}
else {
level.sfx.play_once('bomb', me.cell);
level.transmute_tile(other, 'explosion');
}
},
@ -715,6 +724,8 @@ const TILE_TYPES = {
button_blue: {
draw_layer: LAYER_TERRAIN,
on_arrive(me, level, other) {
level.sfx.play_once('button-press', me.cell);
// Flip direction of all blue tanks
for (let actor of level.actors) {
// TODO generify somehow??
@ -723,10 +734,15 @@ const TILE_TYPES = {
}
}
},
on_depart(me, level, other) {
level.sfx.play_once('button-release', me.cell);
},
},
button_yellow: {
draw_layer: LAYER_TERRAIN,
on_arrive(me, level, other) {
level.sfx.play_once('button-press', me.cell);
// Move all yellow tanks one tile in the direction of the pressing actor
for (let actor of level.actors) {
// TODO generify somehow??
@ -735,10 +751,15 @@ const TILE_TYPES = {
}
}
},
on_depart(me, level, other) {
level.sfx.play_once('button-release', me.cell);
},
},
button_green: {
draw_layer: LAYER_TERRAIN,
on_arrive(me, level, other) {
level.sfx.play_once('button-press', me.cell);
// Swap green floors and walls
// TODO could probably make this more compact for undo purposes
for (let row of level.cells) {
@ -760,12 +781,17 @@ const TILE_TYPES = {
}
}
},
on_depart(me, level, other) {
level.sfx.play_once('button-release', me.cell);
},
},
button_brown: {
draw_layer: LAYER_TERRAIN,
connects_to: 'trap',
connect_order: 'forward',
on_arrive(me, level, other) {
level.sfx.play_once('button-press', me.cell);
if (me.connection && me.connection.cell) {
let trap = me.connection;
level._set_prop(trap, 'open', true);
@ -782,6 +808,9 @@ const TILE_TYPES = {
}
},
on_depart(me, level, other) {
// TODO this doesn't play if you walk straight across
level.sfx.play_once('button-release', me.cell);
if (me.connection && me.connection.cell) {
let trap = me.connection;
level._set_prop(trap, 'open', false);
@ -798,10 +827,15 @@ const TILE_TYPES = {
connects_to: 'cloner',
connect_order: 'forward',
on_arrive(me, level, other) {
level.sfx.play_once('button-press', me.cell);
if (me.connection && me.connection.cell) {
me.connection.type.activate(me.connection, level);
}
},
on_depart(me, level, other) {
level.sfx.play_once('button-release', me.cell);
},
},
button_orange: {
draw_layer: LAYER_TERRAIN,
@ -1233,6 +1267,7 @@ const TILE_TYPES = {
},
on_arrive(me, level, other) {
if (other.type.is_player && level.chips_remaining === 0) {
level.sfx.play_once('socket');
level.transmute_tile(me, 'floor');
}
},

BIN
sfx/bomb.ogg Normal file

Binary file not shown.

BIN
sfx/bummer.ogg Normal file

Binary file not shown.

BIN
sfx/bummer.wav Normal file

Binary file not shown.

BIN
sfx/button-press.ogg Normal file

Binary file not shown.

BIN
sfx/button-release.ogg Normal file

Binary file not shown.

BIN
sfx/door.ogg Normal file

Binary file not shown.

BIN
sfx/get-chip.ogg Normal file

Binary file not shown.

BIN
sfx/get-key.ogg Normal file

Binary file not shown.

BIN
sfx/get-tool.ogg Normal file

Binary file not shown.

BIN
sfx/mmf-high.ogg Normal file

Binary file not shown.

BIN
sfx/mmf-orig.wav Normal file

Binary file not shown.

BIN
sfx/mmf.ogg Normal file

Binary file not shown.

BIN
sfx/socket.ogg Normal file

Binary file not shown.

BIN
sfx/splash.ogg Normal file

Binary file not shown.

BIN
sfx/step-floor.ogg Normal file

Binary file not shown.

BIN
sfx/teleport.ogg Normal file

Binary file not shown.

BIN
sfx/thief.ogg Normal file

Binary file not shown.

BIN
sfx/tick.ogg Normal file

Binary file not shown.

BIN
sfx/timeup.ogg Normal file

Binary file not shown.

BIN
sfx/win.ogg Normal file

Binary file not shown.