Add audio assets and integrate sound effects with enhanced visual feedback
This commit is contained in:
parent
b17cb86c55
commit
4bcbfbd9c9
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -5,6 +5,10 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Overrun</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'FutureImperfect';
|
||||
src: url('assets/fonts/FutureImperfect-2gmo.otf') format('opentype');
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #000;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export class Player {
|
|||
this.hp = this.stats.maxHp;
|
||||
this.lives = 3;
|
||||
this.invincible = false;
|
||||
this._dead = false;
|
||||
this._fireCooldown = 0;
|
||||
this._baseFireInterval = 250; // ms between shots
|
||||
this._isMoving = false;
|
||||
|
|
@ -40,10 +41,10 @@ export class Player {
|
|||
const anims = this.scene.anims;
|
||||
if (anims.exists('player-idle')) return; // already created
|
||||
|
||||
anims.create({ key: 'player-idle', frames: [{ key: 'player', frame: 0 }], frameRate: 1, repeat: -1 });
|
||||
anims.create({ key: 'player-walk', frames: anims.generateFrameNumbers('player', { frames: [1, 2] }), frameRate: 8, repeat: -1 });
|
||||
anims.create({ key: 'player-idle-gun', frames: [{ key: 'player', frame: 3 }], frameRate: 1, repeat: -1 });
|
||||
anims.create({ key: 'player-walk-gun', frames: anims.generateFrameNumbers('player', { frames: [4, 5] }), frameRate: 8, repeat: -1 });
|
||||
anims.create({ key: 'player-idle', frames: [{ key: 'player', frame: 0 }], frameRate: 1, repeat: -1 });
|
||||
anims.create({ key: 'player-walk', frames: anims.generateFrameNumbers('player', { frames: [1, 2] }), frameRate: 8, repeat: -1 });
|
||||
anims.create({ key: 'player-idle-gun', frames: [{ key: 'player', frame: 3 }], frameRate: 1, repeat: -1 });
|
||||
anims.create({ key: 'player-walk-gun', frames: anims.generateFrameNumbers('player', { frames: [4, 5] }), frameRate: 8, repeat: -1 });
|
||||
}
|
||||
|
||||
_buildSprite(x, y) {
|
||||
|
|
@ -69,6 +70,7 @@ export class Player {
|
|||
get active() { return this.sprite?.active ?? false; }
|
||||
|
||||
update(delta) {
|
||||
if (this._dead) return;
|
||||
this._move();
|
||||
this._rotateFacing(delta);
|
||||
this._handleFire(delta);
|
||||
|
|
@ -78,10 +80,10 @@ export class Player {
|
|||
|
||||
_move() {
|
||||
let dx = 0, dy = 0;
|
||||
if (this.keys.left.isDown) dx -= 1;
|
||||
if (this.keys.left.isDown) dx -= 1;
|
||||
if (this.keys.right.isDown) dx += 1;
|
||||
if (this.keys.up.isDown) dy -= 1;
|
||||
if (this.keys.down.isDown) dy += 1;
|
||||
if (this.keys.up.isDown) dy -= 1;
|
||||
if (this.keys.down.isDown) dy += 1;
|
||||
|
||||
this._isMoving = dx !== 0 || dy !== 0;
|
||||
if (dx !== 0 && dy !== 0) { dx *= 0.707; dy *= 0.707; }
|
||||
|
|
@ -109,10 +111,10 @@ export class Player {
|
|||
|
||||
_updateAnimation() {
|
||||
let key;
|
||||
if (this._isMoving && this._isShooting) key = 'player-walk-gun';
|
||||
else if (this._isMoving) key = 'player-walk';
|
||||
else if (this._isShooting) key = 'player-idle-gun';
|
||||
else key = 'player-idle';
|
||||
if (this._isMoving && this._isShooting) key = 'player-walk-gun';
|
||||
else if (this._isMoving) key = 'player-walk';
|
||||
else if (this._isShooting) key = 'player-idle-gun';
|
||||
else key = 'player-idle';
|
||||
|
||||
if (this.sprite.anims.currentAnim?.key !== key) {
|
||||
this.sprite.play(key);
|
||||
|
|
@ -120,6 +122,7 @@ export class Player {
|
|||
}
|
||||
|
||||
_spawnBullet() {
|
||||
this.scene.sound.play('sfx-shoot', { volume: 0.4 });
|
||||
const bx = this.x + Math.cos(this.facing) * GUN_TIP_DIST;
|
||||
const by = this.y + Math.sin(this.facing) * GUN_TIP_DIST;
|
||||
const bullet = this.scene.add.circle(bx, by, BULLET_SIZE, 0xffff00);
|
||||
|
|
@ -149,32 +152,140 @@ export class Player {
|
|||
|
||||
if (this.hp <= 0) {
|
||||
this.hp = 0;
|
||||
this._hitFlash(true);
|
||||
this._loseLife();
|
||||
} else {
|
||||
this._hitFlash(false);
|
||||
}
|
||||
}
|
||||
|
||||
_hitFlash(isLifeLost) {
|
||||
this.scene.sound.play('sfx-take-damage', { volume: 0.5 });
|
||||
const scene = this.scene;
|
||||
const W = scene.scale.width;
|
||||
const H = scene.scale.height;
|
||||
|
||||
scene.cameras.main.shake(isLifeLost ? 300 : 80, isLifeLost ? 0.016 : 0.005);
|
||||
|
||||
const alpha = isLifeLost ? 0.55 : 0.28;
|
||||
const duration = isLifeLost ? 400 : 220;
|
||||
const flash = scene.add.rectangle(W / 2, H / 2, W, H, 0xff0000, alpha).setDepth(49);
|
||||
scene.tweens.add({ targets: flash, alpha: 0, duration, onComplete: () => flash.destroy() });
|
||||
|
||||
if (!isLifeLost) {
|
||||
this.sprite.setTint(0xff4444);
|
||||
scene.time.delayedCall(100, () => { if (this.sprite?.active) this.sprite.clearTint(); });
|
||||
}
|
||||
}
|
||||
|
||||
_loseLife() {
|
||||
const livesBeforeDeath = this.lives;
|
||||
this.lives--;
|
||||
|
||||
if (this.lives <= 0) {
|
||||
this.scene.events.emit('game-over');
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.sound.play('sfx-death', { volume: 0.6 });
|
||||
|
||||
this.hp = this.stats.maxHp;
|
||||
this._startInvincibility();
|
||||
this.invincible = true;
|
||||
this._dead = true;
|
||||
this.sprite.body.setVelocity(0, 0);
|
||||
this.sprite.anims.stop();
|
||||
|
||||
// Spin and fade out
|
||||
this.scene.tweens.add({
|
||||
targets: this.sprite,
|
||||
angle: '+=720',
|
||||
alpha: 0,
|
||||
duration: 600,
|
||||
ease: 'Power2',
|
||||
onComplete: () => {
|
||||
const W = this.scene.scale.width;
|
||||
const H = this.scene.scale.height;
|
||||
this.sprite.setPosition(W / 2, H / 2);
|
||||
this.sprite.setAngle(0);
|
||||
this.sprite.setAlpha(1);
|
||||
this._dead = false;
|
||||
this.scene.sound.play('sfx-new-life', { volume: 0.6 });
|
||||
this._startInvincibility();
|
||||
},
|
||||
});
|
||||
|
||||
this._showLivesOverlay(livesBeforeDeath, this.lives);
|
||||
}
|
||||
|
||||
_startInvincibility() {
|
||||
this.invincible = true;
|
||||
// Blink for ~5 seconds: 17 cycles × 300ms = 5.1s
|
||||
this.scene.tweens.add({
|
||||
targets: this.sprite,
|
||||
alpha: 0,
|
||||
duration: 150,
|
||||
yoyo: true,
|
||||
repeat: 4,
|
||||
repeat: 16,
|
||||
onComplete: () => {
|
||||
this.sprite.setAlpha(1);
|
||||
this.invincible = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_showLivesOverlay(oldLives, newLives) {
|
||||
const scene = this.scene;
|
||||
const W = scene.scale.width;
|
||||
const H = scene.scale.height;
|
||||
const cx = W / 2;
|
||||
const cy = H * 0.3;
|
||||
|
||||
const baseStyle = {
|
||||
fontFamily: 'FutureImperfect',
|
||||
fontSize: '52px',
|
||||
stroke: '#000000', strokeThickness: 6,
|
||||
};
|
||||
|
||||
const label = scene.add.text(cx, cy, 'Lives: ', { ...baseStyle, fill: '#ffffff' })
|
||||
.setOrigin(1, 0.5).setDepth(60);
|
||||
const oldText = scene.add.text(cx, cy, String(oldLives), { ...baseStyle, fill: '#ff4444' })
|
||||
.setOrigin(0, 0.5).setDepth(60);
|
||||
|
||||
// Shake old count, then fade it out, then show new count
|
||||
scene.tweens.add({
|
||||
targets: oldText,
|
||||
x: oldText.x + 12,
|
||||
duration: 50,
|
||||
yoyo: true,
|
||||
repeat: 7,
|
||||
delay: 400,
|
||||
onComplete: () => {
|
||||
scene.tweens.add({
|
||||
targets: oldText,
|
||||
alpha: 0,
|
||||
duration: 180,
|
||||
onComplete: () => {
|
||||
oldText.destroy();
|
||||
const newText = scene.add.text(cx, cy, String(newLives), { ...baseStyle, fill: '#44ff88' })
|
||||
.setOrigin(0, 0.5).setDepth(60).setAlpha(0);
|
||||
scene.tweens.add({
|
||||
targets: newText,
|
||||
alpha: 1,
|
||||
duration: 200,
|
||||
onComplete: () => {
|
||||
scene.time.delayedCall(900, () => {
|
||||
scene.tweens.add({
|
||||
targets: [label, newText],
|
||||
alpha: 0,
|
||||
duration: 1200,
|
||||
onComplete: () => { label.destroy(); newText.destroy(); },
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export class BaseEnemy {
|
|||
|
||||
_die() {
|
||||
this._dying = true;
|
||||
if (this._deathSound) this.scene.sound.play(this._deathSound, { volume: 0.5 });
|
||||
this.scene.events.emit('enemy-killed', { xp: this.xp, x: this.x, y: this.y });
|
||||
|
||||
// Stop movement and hide HP bar immediately
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export class BomberEnemy extends BaseEnemy {
|
|||
this._offsetAngle = Math.random() * Math.PI * 2;
|
||||
this._wobble = 0.3 + Math.random() * 0.4;
|
||||
this._exploded = false;
|
||||
this._deathSound = 'sfx-death-bomber';
|
||||
}
|
||||
|
||||
update(delta) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export class ChaseEnemy extends BaseEnemy {
|
|||
contactDamage: 12,
|
||||
});
|
||||
this._deathPulseColor = 0x00ff44;
|
||||
this._deathSound = 'sfx-death-chaser';
|
||||
}
|
||||
|
||||
update(delta) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export class ShooterEnemy extends BaseEnemy {
|
|||
this._shootTimer = SHOOT_INTERVAL * Math.random(); // stagger first shot
|
||||
this.projectiles = scene.add.group();
|
||||
this._deathPulseColor = 0x9900ff;
|
||||
this._deathSound = 'sfx-death-shooter';
|
||||
}
|
||||
|
||||
_die() {
|
||||
|
|
@ -64,6 +65,7 @@ export class ShooterEnemy extends BaseEnemy {
|
|||
}
|
||||
|
||||
_fireProjectile() {
|
||||
this.scene.sound.play('sfx-shooter-shoot', { volume: 0.35 });
|
||||
const angle = Phaser.Math.Angle.Between(this.x, this.y, this.player.x, this.player.y);
|
||||
const proj = this.scene.add.circle(this.x, this.y, 6, PROJECTILE_COLOR);
|
||||
this.scene.physics.add.existing(proj);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export class SprayerEnemy extends BaseEnemy {
|
|||
this._shootTimer = SHOOT_INTERVAL * Math.random();
|
||||
this.projectiles = scene.add.group();
|
||||
this._deathPulseColor = 0x00ffcc;
|
||||
this._deathSound = 'sfx-death-sprayer';
|
||||
}
|
||||
|
||||
_die() {
|
||||
|
|
@ -63,6 +64,7 @@ export class SprayerEnemy extends BaseEnemy {
|
|||
}
|
||||
|
||||
_fireSpray() {
|
||||
this.scene.sound.play('sfx-sprayer-shoot', { volume: 0.4 });
|
||||
const baseAngle = Phaser.Math.Angle.Between(this.x, this.y, this.player.x, this.player.y);
|
||||
for (let i = 0; i < SPRAY_COUNT; i++) {
|
||||
// Spread bullets evenly across the arc: indices 0..4 map to -1..+1
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export class SwarmerEnemy extends BaseEnemy {
|
|||
this._offsetAngle = Math.random() * Math.PI * 2;
|
||||
this._wobble = Math.random() * 0.5;
|
||||
this._deathPulseColor = 0xff2222;
|
||||
this._deathSound = 'sfx-death-swarmer';
|
||||
}
|
||||
|
||||
_die() {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ export class GameOverScene extends Phaser.Scene {
|
|||
this.add.rectangle(W / 2, H / 2, W, H, 0x000000, 0.75);
|
||||
|
||||
this.add.text(W / 2, H / 2 - 60, 'GAME OVER', {
|
||||
fontFamily: 'FutureImperfect',
|
||||
fontSize: '64px',
|
||||
fill: '#ff3333',
|
||||
fontStyle: 'bold',
|
||||
stroke: '#660000',
|
||||
strokeThickness: 6,
|
||||
}).setOrigin(0.5);
|
||||
|
|
|
|||
|
|
@ -15,8 +15,23 @@ export class GameScene extends Phaser.Scene {
|
|||
preload() {
|
||||
this.load.json('zones', './js/data/zones.json');
|
||||
this.load.json('skillTree', './js/data/skillTree.json');
|
||||
this.load.spritesheet('player', './assets/sprites/player.png', { frameWidth: 48, frameHeight: 48 });
|
||||
this.load.spritesheet('player', './assets/sprites/player.png', { frameWidth: 48, frameHeight: 48 });
|
||||
this.load.spritesheet('enemies', './assets/sprites/enemies.png', { frameWidth: 48, frameHeight: 48 });
|
||||
this.load.audio('music-game', './assets/music/gameBackground.mp3');
|
||||
this.load.audio('sfx-edge-remove', './assets/fx/EdgeRemove.mp3');
|
||||
this.load.audio('sfx-arrow-ping', './assets/fx/ArrowPing.mp3');
|
||||
this.load.audio('sfx-sprayer-shoot', './assets/fx/SprayerShoot.mp3');
|
||||
this.load.audio('sfx-death', './assets/fx/Death.mp3');
|
||||
this.load.audio('sfx-new-life', './assets/fx/NewLife.mp3');
|
||||
this.load.audio('sfx-shoot', './assets/fx/Shoot.mp3');
|
||||
this.load.audio('sfx-take-damage', './assets/fx/TakeDamage.mp3');
|
||||
this.load.audio('sfx-shooter-shoot', './assets/fx/ShooterShoot.mp3');
|
||||
this.load.audio('sfx-enemy-wave', './assets/fx/EnemyWave.mp3');
|
||||
this.load.audio('sfx-death-chaser', './assets/fx/EnemyChaseDeath.mp3');
|
||||
this.load.audio('sfx-death-swarmer', './assets/fx/EnemySwarmerDeath.mp3');
|
||||
this.load.audio('sfx-death-shooter', './assets/fx/EnemyShooterDeath.mp3');
|
||||
this.load.audio('sfx-death-sprayer', './assets/fx/EnemySprayerDeath.mp3');
|
||||
this.load.audio('sfx-death-bomber', './assets/fx/EnemyBomberDeath.mp3');
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -55,12 +70,16 @@ export class GameScene extends Phaser.Scene {
|
|||
this._waitingForExit = false;
|
||||
this._pulseEffects = [];
|
||||
|
||||
this.events.on('wave-start', () => this.sound.play('sfx-enemy-wave', { volume: 0.5 }));
|
||||
this.events.on('zone-waves-complete', () => this._startZoneExit());
|
||||
|
||||
this.reticle = new Reticle(this);
|
||||
|
||||
this._setupBarriers();
|
||||
|
||||
this._bgMusic = this.sound.add('music-game', { loop: true, volume: 0.8 });
|
||||
this._bgMusic.play();
|
||||
|
||||
this.waveManager.start();
|
||||
}
|
||||
|
||||
|
|
@ -191,6 +210,7 @@ export class GameScene extends Phaser.Scene {
|
|||
// ── Zone exit (Smash TV style) ─────────────────────────────────────────────
|
||||
|
||||
_startZoneExit() {
|
||||
this.sound.play('sfx-edge-remove', { volume: 0.6 });
|
||||
// Explode edge barriers first, then allow exit
|
||||
this.barrierManager.explodeEdgeBarriers(() => {
|
||||
this._waitingForExit = true;
|
||||
|
|
@ -209,11 +229,11 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
const ARROW_W = 30; // triangle base half-width
|
||||
const ARROW_D = 28; // triangle depth (pointing direction)
|
||||
const MARGIN = 18; // gap from screen edge to arrow tip
|
||||
const MARGIN = 18; // gap from screen edge to arrow tip
|
||||
|
||||
const drawUp = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY + ARROW_D, cx + ARROW_W, tipY + ARROW_D, cx, tipY);
|
||||
const drawDown = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY - ARROW_D, cx + ARROW_W, tipY - ARROW_D, cx, tipY);
|
||||
const drawLeft = (tipX, cy) => g.fillTriangle(tipX + ARROW_D, cy - ARROW_W, tipX + ARROW_D, cy + ARROW_W, tipX, cy);
|
||||
const drawUp = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY + ARROW_D, cx + ARROW_W, tipY + ARROW_D, cx, tipY);
|
||||
const drawDown = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY - ARROW_D, cx + ARROW_W, tipY - ARROW_D, cx, tipY);
|
||||
const drawLeft = (tipX, cy) => g.fillTriangle(tipX + ARROW_D, cy - ARROW_W, tipX + ARROW_D, cy + ARROW_W, tipX, cy);
|
||||
const drawRight = (tipX, cy) => g.fillTriangle(tipX - ARROW_D, cy - ARROW_W, tipX - ARROW_D, cy + ARROW_W, tipX, cy);
|
||||
|
||||
// Three arrows per edge
|
||||
|
|
@ -230,6 +250,9 @@ export class GameScene extends Phaser.Scene {
|
|||
drawRight(W - MARGIN, cy);
|
||||
});
|
||||
|
||||
this._arrowPing = this.sound.add('sfx-arrow-ping', { loop: true, volume: 0.4 });
|
||||
this._arrowPing.play();
|
||||
|
||||
// Blink the arrows
|
||||
this._exitArrowsTween = this.tweens.add({
|
||||
targets: g,
|
||||
|
|
@ -262,6 +285,8 @@ export class GameScene extends Phaser.Scene {
|
|||
this._exitArrowsTween?.stop();
|
||||
this._exitArrowsGfx?.destroy();
|
||||
this._exitText?.destroy();
|
||||
this._arrowPing?.stop();
|
||||
this._arrowPing = null;
|
||||
this._exitArrowsGfx = null;
|
||||
this._exitText = null;
|
||||
}
|
||||
|
|
@ -272,9 +297,9 @@ export class GameScene extends Phaser.Scene {
|
|||
const px = this.player.x;
|
||||
const py = this.player.y;
|
||||
|
||||
if (px < -24) this._doZoneTransition('left');
|
||||
if (px < -24) this._doZoneTransition('left');
|
||||
else if (px > W + 24) this._doZoneTransition('right');
|
||||
else if (py < -24) this._doZoneTransition('top');
|
||||
else if (py < -24) this._doZoneTransition('top');
|
||||
else if (py > H + 24) this._doZoneTransition('bottom');
|
||||
}
|
||||
|
||||
|
|
@ -289,7 +314,7 @@ export class GameScene extends Phaser.Scene {
|
|||
const px = Phaser.Math.Clamp(this.player.x, 48, W - 48);
|
||||
const py = Phaser.Math.Clamp(this.player.y, 48, H - 48);
|
||||
const spawnX = direction === 'left' ? W - 48 : direction === 'right' ? 48 : px;
|
||||
const spawnY = direction === 'top' ? H - 48 : direction === 'bottom' ? 48 : py;
|
||||
const spawnY = direction === 'top' ? H - 48 : direction === 'bottom' ? 48 : py;
|
||||
|
||||
// White flash
|
||||
const flash = this.add.rectangle(W / 2, H / 2, W, H, 0xffffff).setDepth(50);
|
||||
|
|
@ -341,7 +366,8 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
this.add.rectangle(W / 2, H / 2, W, H, 0x000000, 0.7).setDepth(30);
|
||||
this.add.text(W / 2, H / 2 - 40, 'VICTORY!', {
|
||||
fontSize: '72px', fill: '#ffdd00', fontStyle: 'bold'
|
||||
fontFamily: 'FutureImperfect',
|
||||
fontSize: '72px', fill: '#ffdd00',
|
||||
}).setOrigin(0.5).setDepth(31);
|
||||
|
||||
const prompt = this.add.text(W / 2, H / 2 + 60, 'Press R to return to menu', {
|
||||
|
|
@ -364,6 +390,8 @@ export class GameScene extends Phaser.Scene {
|
|||
this.player?.destroy();
|
||||
this.waveManager?.reset();
|
||||
this.hud?.destroy();
|
||||
this._bgMusic?.stop();
|
||||
this._bgMusic = null;
|
||||
this._enemyProjectiles = [];
|
||||
this._pulseEffects = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ export class IntroScene extends Phaser.Scene {
|
|||
this.add.rectangle(W / 2, H / 2, W, H, 0x000000);
|
||||
|
||||
this.add.text(W / 2, H / 2 - 100, 'OVERRUN', {
|
||||
fontFamily: 'FutureImperfect',
|
||||
fontSize: '72px',
|
||||
fill: '#00ccff',
|
||||
fontStyle: 'bold',
|
||||
stroke: '#003366',
|
||||
strokeThickness: 6,
|
||||
}).setOrigin(0.5);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ export class SkillTreeUI {
|
|||
|
||||
// Title
|
||||
const title = this.scene.add.text(W / 2, H / 2 - 160, `LEVEL UP!`, {
|
||||
fontSize: '36px', fill: '#ffdd44', fontStyle: 'bold'
|
||||
fontFamily: 'FutureImperfect',
|
||||
fontSize: '36px', fill: '#ffdd44',
|
||||
}).setOrigin(0.5).setDepth(21);
|
||||
this._elements.push(title);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue