386 lines
10 KiB
JavaScript
386 lines
10 KiB
JavaScript
/**
|
|
* LIVING NOIR CITY - AMBIENT WORLD SYSTEM
|
|
* Stray cats, barking dogs, atmospheric sounds
|
|
*/
|
|
|
|
class NoirCitySystem {
|
|
constructor(scene) {
|
|
this.scene = scene;
|
|
this.animals = [];
|
|
this.ambientSounds = [];
|
|
this.isActive = false;
|
|
}
|
|
|
|
/**
|
|
* INITIALIZE CITY ATMOSPHERE
|
|
*/
|
|
init() {
|
|
this.spawnStrayAnimals();
|
|
this.setupAmbientSounds();
|
|
this.startAtmosphere();
|
|
this.isActive = true;
|
|
|
|
console.log('🌆 Noir city atmosphere activated');
|
|
}
|
|
|
|
/**
|
|
* SPAWN STRAY CATS
|
|
*/
|
|
spawnStrayAnimals() {
|
|
// Spawn 3-5 stray cats in random locations
|
|
const catCount = Phaser.Math.Between(3, 5);
|
|
|
|
for (let i = 0; i < catCount; i++) {
|
|
this.spawnCat();
|
|
}
|
|
|
|
// Spawn 2-3 dogs in shadows
|
|
const dogCount = Phaser.Math.Between(2, 3);
|
|
|
|
for (let i = 0; i < dogCount; i++) {
|
|
this.spawnDog();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SPAWN A STRAY CAT
|
|
*/
|
|
spawnCat() {
|
|
const x = Phaser.Math.Between(100, this.scene.cameras.main.width - 100);
|
|
const y = Phaser.Math.Between(100, this.scene.cameras.main.height - 100);
|
|
|
|
const cat = this.scene.physics.add.sprite(x, y, 'stray_cat');
|
|
cat.setDepth(15);
|
|
cat.setScale(0.8);
|
|
|
|
// Cat behavior
|
|
cat.animalType = 'cat';
|
|
cat.state = 'idle'; // 'idle', 'running', 'hiding'
|
|
cat.runSpeed = 200;
|
|
|
|
// Random idle movement
|
|
this.scene.time.addEvent({
|
|
delay: Phaser.Math.Between(3000, 8000),
|
|
callback: () => this.catIdleBehavior(cat),
|
|
loop: true
|
|
});
|
|
|
|
this.animals.push(cat);
|
|
|
|
return cat;
|
|
}
|
|
|
|
/**
|
|
* CAT IDLE BEHAVIOR
|
|
*/
|
|
catIdleBehavior(cat) {
|
|
if (!cat || cat.state === 'running') return;
|
|
|
|
const behavior = Phaser.Math.Between(1, 3);
|
|
|
|
switch (behavior) {
|
|
case 1: // Sit and clean
|
|
cat.state = 'idle';
|
|
cat.setVelocity(0, 0);
|
|
cat.play('cat_sit', true);
|
|
break;
|
|
|
|
case 2: // Wander
|
|
cat.state = 'idle';
|
|
const wanderX = Phaser.Math.Between(-30, 30);
|
|
const wanderY = Phaser.Math.Between(-30, 30);
|
|
cat.setVelocity(wanderX, wanderY);
|
|
cat.play('cat_walk', true);
|
|
|
|
this.scene.time.delayedCall(2000, () => {
|
|
if (cat) cat.setVelocity(0, 0);
|
|
});
|
|
break;
|
|
|
|
case 3: // Jump on trash can
|
|
cat.play('cat_jump', true);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CAT RUNS AWAY FROM LONGBOARD
|
|
*/
|
|
catRunAway(cat, kai) {
|
|
const distance = Phaser.Math.Distance.Between(cat.x, cat.y, kai.x, kai.y);
|
|
|
|
if (distance < 80 && kai.body.speed > 50) {
|
|
cat.state = 'running';
|
|
|
|
// Run opposite direction from Kai
|
|
const angle = Phaser.Math.Angle.Between(kai.x, kai.y, cat.x, cat.y);
|
|
const velocityX = Math.cos(angle) * cat.runSpeed;
|
|
const velocityY = Math.sin(angle) * cat.runSpeed;
|
|
|
|
cat.setVelocity(velocityX, velocityY);
|
|
cat.play('cat_run', true);
|
|
|
|
// Play meow sound
|
|
if (this.scene.sound.get('cat_meow')) {
|
|
this.scene.sound.play('cat_meow', { volume: 0.3 });
|
|
}
|
|
|
|
// Stop running after escaping
|
|
this.scene.time.delayedCall(1500, () => {
|
|
if (cat) {
|
|
cat.state = 'idle';
|
|
cat.setVelocity(0, 0);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SPAWN A STRAY DOG (barks from shadows)
|
|
*/
|
|
spawnDog() {
|
|
const x = Phaser.Math.Between(50, this.scene.cameras.main.width - 50);
|
|
const y = Phaser.Math.Between(50, this.scene.cameras.main.height - 50);
|
|
|
|
const dog = this.scene.physics.add.sprite(x, y, 'stray_dog');
|
|
dog.setDepth(14);
|
|
dog.setAlpha(0.7); // Slightly transparent (in shadows)
|
|
|
|
dog.animalType = 'dog';
|
|
dog.barkTimer = null;
|
|
|
|
// Random barking
|
|
dog.barkTimer = this.scene.time.addEvent({
|
|
delay: Phaser.Math.Between(8000, 15000),
|
|
callback: () => this.dogBark(dog),
|
|
loop: true
|
|
});
|
|
|
|
this.animals.push(dog);
|
|
|
|
return dog;
|
|
}
|
|
|
|
/**
|
|
* DOG BARKING
|
|
*/
|
|
dogBark(dog) {
|
|
if (!dog) return;
|
|
|
|
dog.play('dog_bark', true);
|
|
|
|
// Play bark sound (spatial audio)
|
|
if (this.scene.sound.get('dog_bark')) {
|
|
this.scene.sound.play('dog_bark', {
|
|
volume: 0.4,
|
|
// Spatial audio based on distance (if available)
|
|
});
|
|
}
|
|
|
|
// Show bark indicator
|
|
const bark = this.scene.add.text(
|
|
dog.x,
|
|
dog.y - 30,
|
|
'WOOF!',
|
|
{
|
|
fontSize: '12px',
|
|
fontFamily: 'Arial',
|
|
color: '#ffffff',
|
|
stroke: '#000000',
|
|
strokeThickness: 3
|
|
}
|
|
);
|
|
bark.setOrigin(0.5);
|
|
bark.setDepth(50);
|
|
bark.setAlpha(0.7);
|
|
|
|
this.scene.tweens.add({
|
|
targets: bark,
|
|
alpha: 0,
|
|
y: bark.y - 20,
|
|
duration: 1000,
|
|
onComplete: () => bark.destroy()
|
|
});
|
|
}
|
|
|
|
/**
|
|
* SETUP AMBIENT SOUNDS
|
|
*/
|
|
setupAmbientSounds() {
|
|
// City ambient loop
|
|
if (this.scene.sound.get('city_ambient')) {
|
|
const cityAmbient = this.scene.sound.add('city_ambient', {
|
|
volume: 0.2,
|
|
loop: true
|
|
});
|
|
cityAmbient.play();
|
|
this.ambientSounds.push(cityAmbient);
|
|
}
|
|
|
|
// Wind ambience
|
|
if (this.scene.sound.get('wind_ambient')) {
|
|
const wind = this.scene.sound.add('wind_ambient', {
|
|
volume: 0.15,
|
|
loop: true
|
|
});
|
|
wind.play();
|
|
this.ambientSounds.push(wind);
|
|
}
|
|
|
|
// Random distant sounds
|
|
this.scene.time.addEvent({
|
|
delay: Phaser.Math.Between(10000, 20000),
|
|
callback: () => this.playRandomDistantSound(),
|
|
loop: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* PLAY RANDOM DISTANT SOUND
|
|
*/
|
|
playRandomDistantSound() {
|
|
const sounds = [
|
|
'distant_siren',
|
|
'metal_clang',
|
|
'glass_break',
|
|
'trash_can_fall',
|
|
'crow_caw'
|
|
];
|
|
|
|
const randomSound = Phaser.Utils.Array.GetRandom(sounds);
|
|
|
|
if (this.scene.sound.get(randomSound)) {
|
|
this.scene.sound.play(randomSound, {
|
|
volume: 0.1,
|
|
detune: Phaser.Math.Between(-200, 200) // Vary pitch
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ATMOSPHERIC EFFECTS
|
|
*/
|
|
startAtmosphere() {
|
|
// Dust particles floating
|
|
this.addDustParticles();
|
|
|
|
// Paper/trash blowing in wind
|
|
this.addBlowingTrash();
|
|
|
|
// Flickering streetlights (if night)
|
|
this.addFlickeringLights();
|
|
}
|
|
|
|
/**
|
|
* ADD DUST PARTICLES
|
|
*/
|
|
addDustParticles() {
|
|
if (!this.scene.add.particles) return;
|
|
|
|
const particles = this.scene.add.particles('dust_particle');
|
|
|
|
const emitter = particles.createEmitter({
|
|
x: { min: 0, max: this.scene.cameras.main.width },
|
|
y: -20,
|
|
speedY: { min: 20, max: 50 },
|
|
speedX: { min: -10, max: 10 },
|
|
scale: { start: 0.1, end: 0.3 },
|
|
alpha: { start: 0.3, end: 0 },
|
|
lifespan: 5000,
|
|
frequency: 200,
|
|
quantity: 1
|
|
});
|
|
|
|
emitter.setDepth(5);
|
|
}
|
|
|
|
/**
|
|
* ADD BLOWING TRASH
|
|
*/
|
|
addBlowingTrash() {
|
|
// Spawn occasional paper/trash that blows across screen
|
|
this.scene.time.addEvent({
|
|
delay: Phaser.Math.Between(8000, 15000),
|
|
callback: () => {
|
|
const paper = this.scene.add.sprite(
|
|
-50,
|
|
Phaser.Math.Between(100, this.scene.cameras.main.height - 100),
|
|
'paper_trash'
|
|
);
|
|
paper.setDepth(10);
|
|
|
|
// Blow across screen
|
|
this.scene.tweens.add({
|
|
targets: paper,
|
|
x: this.scene.cameras.main.width + 50,
|
|
angle: 360,
|
|
duration: 8000,
|
|
ease: 'Linear',
|
|
onComplete: () => paper.destroy()
|
|
});
|
|
},
|
|
loop: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* ADD FLICKERING STREETLIGHTS
|
|
*/
|
|
addFlickeringLights() {
|
|
// Find all streetlight sprites
|
|
const lights = this.scene.children.list.filter(child =>
|
|
child.texture && child.texture.key === 'streetlight'
|
|
);
|
|
|
|
lights.forEach(light => {
|
|
// Random flicker
|
|
this.scene.time.addEvent({
|
|
delay: Phaser.Math.Between(2000, 8000),
|
|
callback: () => {
|
|
// Quick flicker
|
|
light.setAlpha(0.3);
|
|
this.scene.time.delayedCall(100, () => {
|
|
light.setAlpha(1);
|
|
this.scene.time.delayedCall(50, () => {
|
|
light.setAlpha(0.3);
|
|
this.scene.time.delayedCall(100, () => {
|
|
light.setAlpha(1);
|
|
});
|
|
});
|
|
});
|
|
},
|
|
loop: true
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* UPDATE (called every frame)
|
|
*/
|
|
update(kai) {
|
|
if (!this.isActive) return;
|
|
|
|
// Update cat behavior (run from Kai if on longboard)
|
|
this.animals.forEach(animal => {
|
|
if (animal.animalType === 'cat') {
|
|
this.catRunAway(animal, kai);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* DESTROY ALL ANIMALS AND SOUNDS
|
|
*/
|
|
destroy() {
|
|
this.animals.forEach(animal => animal.destroy());
|
|
this.animals = [];
|
|
|
|
this.ambientSounds.forEach(sound => sound.stop());
|
|
this.ambientSounds = [];
|
|
|
|
this.isActive = false;
|
|
}
|
|
}
|
|
|
|
export default NoirCitySystem;
|