Add sound effects!
This commit is contained in:
parent
6aee8ed622
commit
40aa845e92
31
js/game.js
31
js/game.js
@ -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;
|
||||
|
||||
129
js/main.js
129
js/main.js
@ -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,
|
||||
|
||||
@ -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
BIN
sfx/bomb.ogg
Normal file
Binary file not shown.
BIN
sfx/bummer.ogg
Normal file
BIN
sfx/bummer.ogg
Normal file
Binary file not shown.
BIN
sfx/bummer.wav
Normal file
BIN
sfx/bummer.wav
Normal file
Binary file not shown.
BIN
sfx/button-press.ogg
Normal file
BIN
sfx/button-press.ogg
Normal file
Binary file not shown.
BIN
sfx/button-release.ogg
Normal file
BIN
sfx/button-release.ogg
Normal file
Binary file not shown.
BIN
sfx/door.ogg
Normal file
BIN
sfx/door.ogg
Normal file
Binary file not shown.
BIN
sfx/get-chip.ogg
Normal file
BIN
sfx/get-chip.ogg
Normal file
Binary file not shown.
BIN
sfx/get-key.ogg
Normal file
BIN
sfx/get-key.ogg
Normal file
Binary file not shown.
BIN
sfx/get-tool.ogg
Normal file
BIN
sfx/get-tool.ogg
Normal file
Binary file not shown.
BIN
sfx/mmf-high.ogg
Normal file
BIN
sfx/mmf-high.ogg
Normal file
Binary file not shown.
BIN
sfx/mmf-orig.wav
Normal file
BIN
sfx/mmf-orig.wav
Normal file
Binary file not shown.
BIN
sfx/mmf.ogg
Normal file
BIN
sfx/mmf.ogg
Normal file
Binary file not shown.
BIN
sfx/socket.ogg
Normal file
BIN
sfx/socket.ogg
Normal file
Binary file not shown.
BIN
sfx/splash.ogg
Normal file
BIN
sfx/splash.ogg
Normal file
Binary file not shown.
BIN
sfx/step-floor.ogg
Normal file
BIN
sfx/step-floor.ogg
Normal file
Binary file not shown.
BIN
sfx/teleport.ogg
Normal file
BIN
sfx/teleport.ogg
Normal file
Binary file not shown.
BIN
sfx/thief.ogg
Normal file
BIN
sfx/thief.ogg
Normal file
Binary file not shown.
BIN
sfx/tick.ogg
Normal file
BIN
sfx/tick.ogg
Normal file
Binary file not shown.
BIN
sfx/timeup.ogg
Normal file
BIN
sfx/timeup.ogg
Normal file
Binary file not shown.
BIN
sfx/win.ogg
Normal file
BIN
sfx/win.ogg
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user