ok
This commit is contained in:
634
DEBUG_TOTAL_RECOVERY/BuildingVisualsSystem.js
Normal file
634
DEBUG_TOTAL_RECOVERY/BuildingVisualsSystem.js
Normal file
@@ -0,0 +1,634 @@
|
||||
/**
|
||||
* BUILDING VISUALS & GENETICS SYSTEM
|
||||
* Advanced animations for farm automation buildings and genetics lab
|
||||
*/
|
||||
class BuildingVisualsSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.enabled = true;
|
||||
|
||||
// Building animations
|
||||
this.buildings = new Map();
|
||||
this.conveyorBelts = [];
|
||||
this.windmills = [];
|
||||
this.silos = [];
|
||||
|
||||
// Genetics lab
|
||||
this.geneticsLab = null;
|
||||
this.dnaHelixAnimation = null;
|
||||
this.mutationVats = [];
|
||||
|
||||
// Settings
|
||||
this.settings = {
|
||||
buildingAnimations: true,
|
||||
particleEffects: true,
|
||||
geneticsUI: true,
|
||||
animationSpeed: 1.0
|
||||
};
|
||||
|
||||
this.loadSettings();
|
||||
this.init();
|
||||
|
||||
console.log('✅ Building Visuals System initialized');
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log('🏭 Building animations ready');
|
||||
}
|
||||
|
||||
// ========== AUTO-PLANTER ==========
|
||||
|
||||
createAutoPlanter(x, y) {
|
||||
if (!this.settings.buildingAnimations) return null;
|
||||
|
||||
const planter = {
|
||||
x, y,
|
||||
arm: null,
|
||||
seed: null,
|
||||
isPlanting: false
|
||||
};
|
||||
|
||||
// Create mechanical arm
|
||||
const arm = this.scene.add.graphics();
|
||||
arm.lineStyle(4, 0x888888, 1);
|
||||
arm.lineBetween(0, 0, 0, 30); // Vertical arm
|
||||
arm.lineBetween(0, 30, 20, 30); // Horizontal extension
|
||||
arm.setPosition(x, y);
|
||||
arm.setDepth(100);
|
||||
planter.arm = arm;
|
||||
|
||||
this.buildings.set(`planter_${x}_${y}`, planter);
|
||||
return planter;
|
||||
}
|
||||
|
||||
animatePlanting(planter, targetX, targetY) {
|
||||
if (!planter || planter.isPlanting) return;
|
||||
|
||||
planter.isPlanting = true;
|
||||
|
||||
// Arm extends down
|
||||
this.scene.tweens.add({
|
||||
targets: planter.arm,
|
||||
y: planter.y + 20,
|
||||
duration: 500,
|
||||
ease: 'Power2',
|
||||
onComplete: () => {
|
||||
// Drop seed
|
||||
this.createSeedParticle(targetX, targetY);
|
||||
|
||||
// Arm retracts
|
||||
this.scene.tweens.add({
|
||||
targets: planter.arm,
|
||||
y: planter.y,
|
||||
duration: 500,
|
||||
ease: 'Power2',
|
||||
onComplete: () => {
|
||||
planter.isPlanting = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createSeedParticle(x, y) {
|
||||
const seed = this.scene.add.circle(x, y - 20, 3, 0x8B4513);
|
||||
seed.setDepth(99);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: seed,
|
||||
y: y,
|
||||
duration: 300,
|
||||
ease: 'Bounce.easeOut',
|
||||
onComplete: () => {
|
||||
// Puff of dirt
|
||||
if (this.scene.visualEnhancements) {
|
||||
this.scene.visualEnhancements.createSparkleEffect(x, y);
|
||||
}
|
||||
seed.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== AUTO-HARVESTER ==========
|
||||
|
||||
createAutoHarvester(x, y) {
|
||||
if (!this.settings.buildingAnimations) return null;
|
||||
|
||||
const harvester = {
|
||||
x, y,
|
||||
blades: [],
|
||||
isHarvesting: false,
|
||||
rotation: 0
|
||||
};
|
||||
|
||||
// Create spinning blades
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const blade = this.scene.add.graphics();
|
||||
blade.lineStyle(3, 0xC0C0C0, 1);
|
||||
blade.lineBetween(0, 0, 15, 0);
|
||||
blade.setPosition(x, y);
|
||||
blade.setRotation((Math.PI / 2) * i);
|
||||
blade.setDepth(100);
|
||||
harvester.blades.push(blade);
|
||||
}
|
||||
|
||||
this.buildings.set(`harvester_${x}_${y}`, harvester);
|
||||
return harvester;
|
||||
}
|
||||
|
||||
animateHarvesting(harvester, cropX, cropY) {
|
||||
if (!harvester || harvester.isHarvesting) return;
|
||||
|
||||
harvester.isHarvesting = true;
|
||||
|
||||
// Spin blades
|
||||
for (const blade of harvester.blades) {
|
||||
this.scene.tweens.add({
|
||||
targets: blade,
|
||||
rotation: blade.rotation + Math.PI * 4,
|
||||
duration: 1000,
|
||||
ease: 'Linear'
|
||||
});
|
||||
}
|
||||
|
||||
// Move to crop
|
||||
this.scene.tweens.add({
|
||||
targets: harvester.blades,
|
||||
x: cropX,
|
||||
y: cropY,
|
||||
duration: 500,
|
||||
ease: 'Power2',
|
||||
onComplete: () => {
|
||||
// Harvest effect
|
||||
this.createHarvestEffect(cropX, cropY);
|
||||
|
||||
// Return to base
|
||||
this.scene.tweens.add({
|
||||
targets: harvester.blades,
|
||||
x: harvester.x,
|
||||
y: harvester.y,
|
||||
duration: 500,
|
||||
ease: 'Power2',
|
||||
onComplete: () => {
|
||||
harvester.isHarvesting = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createHarvestEffect(x, y) {
|
||||
// Crop particles
|
||||
const emitter = this.scene.add.particles(x, y, 'particle_white', {
|
||||
speed: { min: 30, max: 60 },
|
||||
scale: { start: 0.5, end: 0 },
|
||||
alpha: { start: 1, end: 0 },
|
||||
lifespan: 800,
|
||||
quantity: 10,
|
||||
tint: 0xFFD700
|
||||
});
|
||||
|
||||
this.scene.time.delayedCall(800, () => emitter.destroy());
|
||||
}
|
||||
|
||||
// ========== CONVEYOR BELT ==========
|
||||
|
||||
createConveyorBelt(x, y, width, direction = 'right') {
|
||||
if (!this.settings.buildingAnimations) return null;
|
||||
|
||||
const belt = {
|
||||
x, y, width, direction,
|
||||
items: [],
|
||||
speed: 50 // pixels per second
|
||||
};
|
||||
|
||||
// Create belt graphics
|
||||
const graphics = this.scene.add.graphics();
|
||||
graphics.fillStyle(0x444444, 1);
|
||||
graphics.fillRect(x, y, width, 20);
|
||||
|
||||
// Moving lines to show direction
|
||||
for (let i = 0; i < width; i += 20) {
|
||||
const line = this.scene.add.rectangle(
|
||||
x + i,
|
||||
y + 10,
|
||||
10,
|
||||
2,
|
||||
0x888888
|
||||
);
|
||||
line.setDepth(99);
|
||||
|
||||
// Animate line movement
|
||||
const targetX = direction === 'right' ? x + width : x;
|
||||
this.scene.tweens.add({
|
||||
targets: line,
|
||||
x: targetX,
|
||||
duration: (width / belt.speed) * 1000,
|
||||
repeat: -1,
|
||||
ease: 'Linear'
|
||||
});
|
||||
}
|
||||
|
||||
belt.graphics = graphics;
|
||||
this.conveyorBelts.push(belt);
|
||||
return belt;
|
||||
}
|
||||
|
||||
addItemToBelt(belt, itemSprite) {
|
||||
if (!belt) return;
|
||||
|
||||
belt.items.push(itemSprite);
|
||||
itemSprite.setPosition(belt.x, belt.y + 10);
|
||||
itemSprite.setDepth(100);
|
||||
|
||||
// Move item along belt
|
||||
const targetX = belt.direction === 'right'
|
||||
? belt.x + belt.width
|
||||
: belt.x;
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: itemSprite,
|
||||
x: targetX,
|
||||
duration: (belt.width / belt.speed) * 1000,
|
||||
ease: 'Linear',
|
||||
onComplete: () => {
|
||||
// Remove from belt
|
||||
const index = belt.items.indexOf(itemSprite);
|
||||
if (index > -1) {
|
||||
belt.items.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== WINDMILL ==========
|
||||
|
||||
createWindmill(x, y) {
|
||||
if (!this.settings.buildingAnimations) return null;
|
||||
|
||||
const windmill = {
|
||||
x, y,
|
||||
blades: null,
|
||||
powerGlow: null,
|
||||
isPowered: false,
|
||||
rotation: 0
|
||||
};
|
||||
|
||||
// Create blades
|
||||
const blades = this.scene.add.graphics();
|
||||
blades.lineStyle(4, 0x8B4513, 1);
|
||||
|
||||
// Draw 4 blades
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const angle = (Math.PI / 2) * i;
|
||||
const endX = Math.cos(angle) * 30;
|
||||
const endY = Math.sin(angle) * 30;
|
||||
blades.lineBetween(0, 0, endX, endY);
|
||||
}
|
||||
|
||||
blades.setPosition(x, y);
|
||||
blades.setDepth(100);
|
||||
windmill.blades = blades;
|
||||
|
||||
// Power glow
|
||||
const glow = this.scene.add.circle(x, y, 40, 0x00ffff, 0);
|
||||
glow.setBlendMode(Phaser.BlendModes.ADD);
|
||||
glow.setDepth(99);
|
||||
windmill.powerGlow = glow;
|
||||
|
||||
// Rotate blades
|
||||
this.scene.tweens.add({
|
||||
targets: blades,
|
||||
rotation: Math.PI * 2,
|
||||
duration: 3000 / this.settings.animationSpeed,
|
||||
repeat: -1,
|
||||
ease: 'Linear'
|
||||
});
|
||||
|
||||
this.windmills.push(windmill);
|
||||
return windmill;
|
||||
}
|
||||
|
||||
setPowerState(windmill, powered) {
|
||||
if (!windmill) return;
|
||||
|
||||
windmill.isPowered = powered;
|
||||
|
||||
if (powered) {
|
||||
// Glow on
|
||||
this.scene.tweens.add({
|
||||
targets: windmill.powerGlow,
|
||||
alpha: 0.4,
|
||||
duration: 500
|
||||
});
|
||||
|
||||
// Particle trail
|
||||
if (this.settings.particleEffects) {
|
||||
const emitter = this.scene.add.particles(
|
||||
windmill.x,
|
||||
windmill.y,
|
||||
'particle_white',
|
||||
{
|
||||
speed: 20,
|
||||
scale: { start: 0.3, end: 0 },
|
||||
alpha: { start: 0.6, end: 0 },
|
||||
lifespan: 1000,
|
||||
frequency: 200,
|
||||
quantity: 1,
|
||||
tint: 0x00ffff
|
||||
}
|
||||
);
|
||||
windmill.particleEmitter = emitter;
|
||||
}
|
||||
} else {
|
||||
// Glow off
|
||||
this.scene.tweens.add({
|
||||
targets: windmill.powerGlow,
|
||||
alpha: 0,
|
||||
duration: 500
|
||||
});
|
||||
|
||||
// Stop particles
|
||||
if (windmill.particleEmitter) {
|
||||
windmill.particleEmitter.destroy();
|
||||
windmill.particleEmitter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== STORAGE SILO ==========
|
||||
|
||||
createStorageSilo(x, y, capacity = 1000) {
|
||||
if (!this.settings.buildingAnimations) return null;
|
||||
|
||||
const silo = {
|
||||
x, y,
|
||||
capacity,
|
||||
currentAmount: 0,
|
||||
fillIndicator: null,
|
||||
fillBar: null
|
||||
};
|
||||
|
||||
// Silo structure
|
||||
const structure = this.scene.add.graphics();
|
||||
structure.fillStyle(0x666666, 1);
|
||||
structure.fillRect(x - 20, y - 60, 40, 60);
|
||||
structure.fillCircle(x, y - 60, 20);
|
||||
structure.setDepth(100);
|
||||
silo.structure = structure;
|
||||
|
||||
// Fill indicator (inside silo)
|
||||
const fillBar = this.scene.add.rectangle(
|
||||
x,
|
||||
y,
|
||||
36,
|
||||
0,
|
||||
0xFFD700,
|
||||
0.8
|
||||
);
|
||||
fillBar.setOrigin(0.5, 1);
|
||||
fillBar.setDepth(101);
|
||||
silo.fillBar = fillBar;
|
||||
|
||||
// Percentage text
|
||||
const text = this.scene.add.text(x, y - 70, '0%', {
|
||||
fontSize: '12px',
|
||||
color: '#ffffff',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
text.setOrigin(0.5);
|
||||
text.setDepth(102);
|
||||
silo.text = text;
|
||||
|
||||
this.silos.push(silo);
|
||||
return silo;
|
||||
}
|
||||
|
||||
updateSiloFill(silo, amount) {
|
||||
if (!silo) return;
|
||||
|
||||
silo.currentAmount = Phaser.Math.Clamp(amount, 0, silo.capacity);
|
||||
const percentage = (silo.currentAmount / silo.capacity) * 100;
|
||||
const fillHeight = (percentage / 100) * 56; // Max height
|
||||
|
||||
// Animate fill
|
||||
this.scene.tweens.add({
|
||||
targets: silo.fillBar,
|
||||
height: fillHeight,
|
||||
duration: 500,
|
||||
ease: 'Power2'
|
||||
});
|
||||
|
||||
// Update text
|
||||
silo.text.setText(`${Math.round(percentage)}%`);
|
||||
}
|
||||
|
||||
// ========== GENETICS LAB ==========
|
||||
|
||||
createGeneticsLab(x, y) {
|
||||
if (!this.settings.geneticsUI) return null;
|
||||
|
||||
const lab = {
|
||||
x, y,
|
||||
dnaHelix: null,
|
||||
vats: [],
|
||||
isActive: false
|
||||
};
|
||||
|
||||
// Lab structure
|
||||
const structure = this.scene.add.rectangle(x, y, 80, 60, 0x333333);
|
||||
structure.setStrokeStyle(2, 0x00ff00);
|
||||
structure.setDepth(100);
|
||||
lab.structure = structure;
|
||||
|
||||
this.geneticsLab = lab;
|
||||
return lab;
|
||||
}
|
||||
|
||||
showDNAHelixAnimation(x, y) {
|
||||
if (!this.settings.geneticsUI) return null;
|
||||
|
||||
// Create DNA helix
|
||||
const helix = this.scene.add.container(x, y);
|
||||
helix.setDepth(200);
|
||||
|
||||
// Two strands
|
||||
const strand1 = [];
|
||||
const strand2 = [];
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const yPos = i * 10 - 100;
|
||||
const angle = (i / 20) * Math.PI * 4;
|
||||
|
||||
const x1 = Math.cos(angle) * 15;
|
||||
const x2 = Math.cos(angle + Math.PI) * 15;
|
||||
|
||||
const dot1 = this.scene.add.circle(x1, yPos, 3, 0x00ff00);
|
||||
const dot2 = this.scene.add.circle(x2, yPos, 3, 0x00ffff);
|
||||
|
||||
helix.add(dot1);
|
||||
helix.add(dot2);
|
||||
|
||||
strand1.push(dot1);
|
||||
strand2.push(dot2);
|
||||
|
||||
// Connecting lines
|
||||
if (i % 3 === 0) {
|
||||
const line = this.scene.add.graphics();
|
||||
line.lineStyle(1, 0xffffff, 0.5);
|
||||
line.lineBetween(x1, yPos, x2, yPos);
|
||||
helix.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate helix
|
||||
this.scene.tweens.add({
|
||||
targets: helix,
|
||||
rotation: Math.PI * 2,
|
||||
duration: 4000,
|
||||
repeat: -1,
|
||||
ease: 'Linear'
|
||||
});
|
||||
|
||||
// Pulse effect
|
||||
this.scene.tweens.add({
|
||||
targets: helix,
|
||||
scale: { from: 1, to: 1.1 },
|
||||
duration: 2000,
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
this.dnaHelixAnimation = helix;
|
||||
return helix;
|
||||
}
|
||||
|
||||
hideDNAHelixAnimation() {
|
||||
if (this.dnaHelixAnimation) {
|
||||
this.dnaHelixAnimation.destroy();
|
||||
this.dnaHelixAnimation = null;
|
||||
}
|
||||
}
|
||||
|
||||
createMutationVat(x, y) {
|
||||
if (!this.settings.geneticsUI) return null;
|
||||
|
||||
const vat = {
|
||||
x, y,
|
||||
container: null,
|
||||
liquid: null,
|
||||
bubbles: null,
|
||||
isActive: false
|
||||
};
|
||||
|
||||
// Vat container
|
||||
const container = this.scene.add.graphics();
|
||||
container.fillStyle(0x444444, 1);
|
||||
container.fillRect(x - 20, y - 40, 40, 40);
|
||||
container.lineStyle(2, 0x666666);
|
||||
container.strokeRect(x - 20, y - 40, 40, 40);
|
||||
container.setDepth(100);
|
||||
vat.container = container;
|
||||
|
||||
// Liquid
|
||||
const liquid = this.scene.add.rectangle(
|
||||
x,
|
||||
y - 20,
|
||||
36,
|
||||
36,
|
||||
0x00ff00,
|
||||
0.6
|
||||
);
|
||||
liquid.setDepth(101);
|
||||
vat.liquid = liquid;
|
||||
|
||||
this.mutationVats.push(vat);
|
||||
return vat;
|
||||
}
|
||||
|
||||
activateMutationVat(vat) {
|
||||
if (!vat || vat.isActive) return;
|
||||
|
||||
vat.isActive = true;
|
||||
|
||||
// Bubbling effect
|
||||
const emitter = this.scene.add.particles(
|
||||
vat.x,
|
||||
vat.y - 40,
|
||||
'particle_white',
|
||||
{
|
||||
speedY: { min: -30, max: -50 },
|
||||
speedX: { min: -5, max: 5 },
|
||||
scale: { start: 0.2, end: 0 },
|
||||
alpha: { start: 0.8, end: 0 },
|
||||
lifespan: 1000,
|
||||
frequency: 200,
|
||||
quantity: 2,
|
||||
tint: 0x00ff00
|
||||
}
|
||||
);
|
||||
vat.bubbles = emitter;
|
||||
|
||||
// Lightning effect
|
||||
if (this.settings.particleEffects) {
|
||||
this.createLightningEffect(vat.x, vat.y - 60);
|
||||
}
|
||||
|
||||
// Liquid glow pulse
|
||||
this.scene.tweens.add({
|
||||
targets: vat.liquid,
|
||||
alpha: { from: 0.6, to: 0.9 },
|
||||
duration: 800,
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
}
|
||||
|
||||
createLightningEffect(x, y) {
|
||||
const lightning = this.scene.add.graphics();
|
||||
lightning.lineStyle(2, 0xffff00, 1);
|
||||
|
||||
// Zigzag lightning
|
||||
let currentX = x;
|
||||
let currentY = y;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const nextX = currentX + (Math.random() - 0.5) * 20;
|
||||
const nextY = currentY + 10;
|
||||
lightning.lineBetween(currentX, currentY, nextX, nextY);
|
||||
currentX = nextX;
|
||||
currentY = nextY;
|
||||
}
|
||||
|
||||
lightning.setDepth(200);
|
||||
|
||||
// Flash and fade
|
||||
this.scene.tweens.add({
|
||||
targets: lightning,
|
||||
alpha: 0,
|
||||
duration: 200,
|
||||
onComplete: () => lightning.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
// ========== SETTINGS ==========
|
||||
|
||||
saveSettings() {
|
||||
localStorage.setItem('novafarma_building_visuals', JSON.stringify(this.settings));
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const saved = localStorage.getItem('novafarma_building_visuals');
|
||||
if (saved) {
|
||||
this.settings = { ...this.settings, ...JSON.parse(saved) };
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.dnaHelixAnimation) this.dnaHelixAnimation.destroy();
|
||||
for (const vat of this.mutationVats) {
|
||||
if (vat.bubbles) vat.bubbles.destroy();
|
||||
}
|
||||
console.log('🏭 Building Visuals System destroyed');
|
||||
}
|
||||
}
|
||||
313
DEBUG_TOTAL_RECOVERY/IntroScene.js
Normal file
313
DEBUG_TOTAL_RECOVERY/IntroScene.js
Normal file
@@ -0,0 +1,313 @@
|
||||
// 🎬 INTRO SEQUENCE - 60-SECOND EPIC CINEMATIC
|
||||
// "From Colors to Darkness" - Complete Story
|
||||
// Created: January 10, 2026
|
||||
// Style: Style 32 Dark-Chibi Noir + Polaroid + VHS
|
||||
// Voices: Kai (Rok) + Ana (Petra) + Gronk (Rok deep)
|
||||
|
||||
class IntroScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super({ key: 'IntroScene' });
|
||||
this.skipEnabled = false;
|
||||
this.currentPhase = 0;
|
||||
this.skipPrompt = null;
|
||||
this.currentPolaroid = null;
|
||||
this.currentText = null;
|
||||
this.vhsNoise = null;
|
||||
this.scanlines = null;
|
||||
this.ambientAudio = null;
|
||||
this.currentVoice = null;
|
||||
}
|
||||
|
||||
preload() {
|
||||
console.log('🎬 IntroScene: Loading EPIC 60s assets...');
|
||||
|
||||
// Base path for intro shots
|
||||
const introPath = 'assets/references/intro_shots/';
|
||||
|
||||
// ALL 20 INTRO SHOTS
|
||||
// DREAMY INTRO ASSETS (Generated)
|
||||
const dreamyPath = 'assets/images/intro_sequence/';
|
||||
|
||||
this.load.image('intro_family_portrait', dreamyPath + 'family_portrait_complete_dreamy.png');
|
||||
this.load.image('intro_otac_longboard', dreamyPath + 'otac_longboard_pier_dreamy.png');
|
||||
this.load.image('intro_kai_dreads', dreamyPath + 'kai_first_dreads_family_dreamy.png');
|
||||
this.load.image('intro_ana_barbershop', dreamyPath + 'ana_barbershop_dreads_dreamy.png');
|
||||
this.load.image('intro_birthday_cake', dreamyPath + 'birthday_cake_rd_dreamy.png');
|
||||
this.load.image('intro_virus', introPath + 'virus_xnoir_microscope.png');
|
||||
this.load.image('intro_chaos', dreamyPath + 'chaos_streets_apocalypse_dreamy.png');
|
||||
this.load.image('intro_zombies', dreamyPath + 'zombie_silhouettes_panic_dreamy.png');
|
||||
this.load.image('intro_parents_ghosts', dreamyPath + 'parents_transparent_ghosts_dreamy.png');
|
||||
this.load.image('intro_ana_taken', introPath + 'ana_taken_military.png');
|
||||
this.load.image('intro_kai_alone', introPath + 'kai_alone_basement.png');
|
||||
this.load.image('intro_kai_young', introPath + 'kai_young_timelapse.png');
|
||||
this.load.image('intro_kai_adult', introPath + 'kai_adult_35_timelapse.png');
|
||||
this.load.image('intro_kai_elder', introPath + 'kai_elder_50_timelapse.png');
|
||||
this.load.image('intro_ana_memory', introPath + 'ana_memory_flash_purple.png');
|
||||
this.load.image('intro_bedroom', introPath + 'kai_bedroom_wakeup.png');
|
||||
this.load.image('intro_gronk', introPath + 'gronk_doorway_silhouette.png');
|
||||
this.load.image('intro_twins_childhood', introPath + 'kai_ana_twins_childhood.png');
|
||||
|
||||
// 🎵 AMBIENT MUSIC
|
||||
this.loadAudioSafe('noir_ambience', 'assets/audio/ambient/noir_ambience.mp3');
|
||||
|
||||
// 🎤 KAI VOICES (12 total - ENGLISH)
|
||||
this.loadAudioSafe('kai_01', 'assets/audio/voiceover/kai_en_01.mp3');
|
||||
this.loadAudioSafe('kai_02', 'assets/audio/voiceover/kai_en_02.mp3');
|
||||
this.loadAudioSafe('kai_03', 'assets/audio/voiceover/kai_en_03.mp3');
|
||||
this.loadAudioSafe('kai_04', 'assets/audio/voiceover/kai_en_04.mp3');
|
||||
this.loadAudioSafe('kai_05', 'assets/audio/voiceover/kai_en_05.mp3');
|
||||
this.loadAudioSafe('kai_06', 'assets/audio/voiceover/kai_en_06.mp3');
|
||||
this.loadAudioSafe('kai_07', 'assets/audio/voiceover/kai_en_07.mp3');
|
||||
this.loadAudioSafe('kai_08', 'assets/audio/voiceover/kai_en_08.mp3');
|
||||
this.loadAudioSafe('kai_09', 'assets/audio/voiceover/kai_en_09.mp3');
|
||||
this.loadAudioSafe('kai_10', 'assets/audio/voiceover/kai_en_10.mp3');
|
||||
this.loadAudioSafe('kai_11', 'assets/audio/voiceover/kai_en_11.mp3');
|
||||
this.loadAudioSafe('kai_12', 'assets/audio/voiceover/kai_en_12.mp3');
|
||||
|
||||
// 🎤 ANA VOICES (8 total - ENGLISH)
|
||||
this.loadAudioSafe('ana_01', 'assets/audio/voiceover/ana_en_01.mp3');
|
||||
this.loadAudioSafe('ana_02', 'assets/audio/voiceover/ana_en_02.mp3');
|
||||
this.loadAudioSafe('ana_03', 'assets/audio/voiceover/ana_en_03.mp3');
|
||||
this.loadAudioSafe('ana_04', 'assets/audio/voiceover/ana_en_04.mp3');
|
||||
this.loadAudioSafe('ana_05', 'assets/audio/voiceover/ana_en_05.mp3');
|
||||
this.loadAudioSafe('ana_06', 'assets/audio/voiceover/ana_en_06.mp3');
|
||||
this.loadAudioSafe('ana_07', 'assets/audio/voiceover/ana_en_07.mp3');
|
||||
this.loadAudioSafe('ana_08', 'assets/audio/voiceover/ana_en_08.mp3');
|
||||
|
||||
// 🎤 GRONK VOICE (ENGLISH - Deep UK)
|
||||
this.loadAudioSafe('gronk_01', 'assets/audio/voiceover/gronk_en_01.mp3');
|
||||
}
|
||||
|
||||
loadAudioSafe(key, path) {
|
||||
try {
|
||||
this.load.audio(key, path);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Audio skipped: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
create() {
|
||||
console.log('🎬 IntroScene: Amnesia Start Sequence (Fixed Timing)');
|
||||
this.cameras.main.setBackgroundColor('#000000');
|
||||
|
||||
// 1. SHADER INTEGRATION: Gaussian Blur
|
||||
this.blurFX = null;
|
||||
if (this.cameras.main.postFX) {
|
||||
try {
|
||||
this.blurFX = this.cameras.main.postFX.addBlur(0, 0, 0);
|
||||
} catch (e) {
|
||||
console.warn('⚠️ PostFX not supported');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Blur at 20
|
||||
if (this.blurFX) {
|
||||
this.blurFX.strength = 20; // High blur
|
||||
|
||||
// Tween blur to 0 over 6 seconds (Standard Time)
|
||||
this.tweens.add({
|
||||
targets: this.blurFX,
|
||||
strength: 0,
|
||||
duration: 6000,
|
||||
ease: 'Power2.easeOut',
|
||||
onComplete: () => {
|
||||
this.triggerWakeUp();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fallback if shaders not supported
|
||||
this.cameras.main.alpha = 0;
|
||||
this.tweens.add({
|
||||
targets: this.cameras.main,
|
||||
alpha: 1,
|
||||
duration: 6000,
|
||||
onComplete: () => this.triggerWakeUp()
|
||||
});
|
||||
}
|
||||
|
||||
// 2. FLASHBACK ENGINE (Safe Mode)
|
||||
// Images: Birthday (0s), Longboard (1.5s), Dreads (3.0s)
|
||||
this.flashbackSequence([
|
||||
'intro_birthday_cake',
|
||||
'intro_otac_longboard',
|
||||
'intro_ana_barbershop'
|
||||
]);
|
||||
|
||||
// 3. TYPEWRITER LOGIC (Fixed Timing & No Overlap)
|
||||
// Line 1: 0.5s -> 3.0s
|
||||
this.time.delayedCall(500, () => {
|
||||
this.showDialogue('Vse je zamegljeno... Zakaj me vse boli?', 2500);
|
||||
});
|
||||
|
||||
// Line 2: 4.0s -> End (starts only after Line 1 is cleared)
|
||||
this.time.delayedCall(4000, () => {
|
||||
this.showDialogue('Kdo so ti ljudje v moji glavi?', 2000);
|
||||
});
|
||||
|
||||
// Audio atmosphere
|
||||
this.startAmbientAudio();
|
||||
}
|
||||
|
||||
flashbackSequence(images) {
|
||||
images.forEach((key, index) => {
|
||||
// Check if asset exists before scheduling
|
||||
if (this.textures.exists(key)) {
|
||||
this.time.delayedCall(index * 1500, () => {
|
||||
this.triggerFlashback(key);
|
||||
});
|
||||
} else {
|
||||
console.warn(`⚠️ Missing flash asset: ${key} - Skipping visual, keeping timing.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
triggerFlashback(key) {
|
||||
// Double check existence
|
||||
if (!this.textures.exists(key)) return;
|
||||
|
||||
const width = this.cameras.main.width;
|
||||
const height = this.cameras.main.height;
|
||||
|
||||
const image = this.add.image(width / 2, height / 2, key);
|
||||
|
||||
// Scale to cover most of screen (maintain aspect ratio)
|
||||
const scale = Math.max(width / image.width, height / image.height) * 0.8;
|
||||
image.setScale(scale);
|
||||
|
||||
image.setAlpha(0);
|
||||
image.setDepth(10);
|
||||
image.setBlendMode(Phaser.BlendModes.ADD);
|
||||
|
||||
// Flash in and out
|
||||
this.tweens.add({
|
||||
targets: image,
|
||||
alpha: { from: 0, to: 0.15 },
|
||||
duration: 500,
|
||||
yoyo: true,
|
||||
hold: 500,
|
||||
onComplete: () => {
|
||||
if (image && image.active) image.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Zoom
|
||||
this.tweens.add({
|
||||
targets: image,
|
||||
scale: scale * 1.1,
|
||||
duration: 1500
|
||||
});
|
||||
}
|
||||
|
||||
showDialogue(text, duration) {
|
||||
// OVERLAP FIX: Destroy previous text immediately
|
||||
if (this.currentText) {
|
||||
this.currentText.destroy();
|
||||
this.currentText = null;
|
||||
}
|
||||
|
||||
const width = this.cameras.main.width;
|
||||
const height = this.cameras.main.height;
|
||||
|
||||
const textObj = this.add.text(width / 2, height - 100, '', {
|
||||
fontFamily: 'Courier New',
|
||||
fontSize: '24px',
|
||||
fill: '#ffffff',
|
||||
align: 'center',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 4
|
||||
});
|
||||
textObj.setOrigin(0.5);
|
||||
textObj.setDepth(100);
|
||||
|
||||
this.currentText = textObj; // Track current text
|
||||
|
||||
// Typewriter
|
||||
let charIndex = 0;
|
||||
const speed = 50;
|
||||
|
||||
if (text.length * speed > duration) {
|
||||
// Speed up if text is too long for duration
|
||||
speed = Math.floor(duration / text.length);
|
||||
}
|
||||
|
||||
const timer = this.time.addEvent({
|
||||
delay: speed,
|
||||
callback: () => {
|
||||
if (!textObj.active) return;
|
||||
textObj.text += text[charIndex];
|
||||
charIndex++;
|
||||
|
||||
if (charIndex >= text.length) {
|
||||
timer.remove();
|
||||
// Fade out logic
|
||||
this.time.delayedCall(Math.max(500, duration - (text.length * speed)), () => {
|
||||
if (textObj.active) {
|
||||
this.tweens.add({
|
||||
targets: textObj,
|
||||
alpha: 0,
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
if (textObj.active) textObj.destroy();
|
||||
if (this.currentText === textObj) this.currentText = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
repeat: text.length - 1
|
||||
});
|
||||
}
|
||||
|
||||
startAmbientAudio() {
|
||||
try {
|
||||
if (this.cache.audio.exists('noir_ambience')) {
|
||||
this.ambientAudio = this.sound.add('noir_ambience', {
|
||||
volume: 0.1, // Start very quiet
|
||||
loop: true
|
||||
});
|
||||
this.ambientAudio.play();
|
||||
// Fade in audio
|
||||
this.tweens.add({
|
||||
targets: this.ambientAudio,
|
||||
volume: 0.4,
|
||||
duration: 6000
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Ambient audio not available');
|
||||
}
|
||||
}
|
||||
|
||||
triggerWakeUp() {
|
||||
console.log('⚡ TRANSITION: Wake Up!');
|
||||
|
||||
// 4. TRANSITION TO GAMEPLAY
|
||||
// White Flash
|
||||
this.cameras.main.flash(1000, 255, 255, 255);
|
||||
|
||||
// Stop audio
|
||||
if (this.ambientAudio) {
|
||||
this.tweens.add({
|
||||
targets: this.ambientAudio,
|
||||
volume: 0,
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
if (this.ambientAudio && this.ambientAudio.stop) {
|
||||
this.ambientAudio.stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Switch Scene
|
||||
this.time.delayedCall(1000, () => {
|
||||
// Set flag
|
||||
if (window.gameState && window.gameState.story) {
|
||||
window.gameState.story.isAmnesiaActive = true;
|
||||
}
|
||||
|
||||
this.scene.start('GameScene');
|
||||
});
|
||||
}
|
||||
}
|
||||
358
DEBUG_TOTAL_RECOVERY/MicroFarmSystem.js
Normal file
358
DEBUG_TOTAL_RECOVERY/MicroFarmSystem.js
Normal file
@@ -0,0 +1,358 @@
|
||||
// 🌱 MICRO FARM SYSTEM - Phase 37
|
||||
// Začetna 8x8 parcela z postopno širitvijo
|
||||
|
||||
class MicroFarmSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// MICRO FARM CONFIG
|
||||
this.farmCenterX = 50; // Center of 100x100 map
|
||||
this.farmCenterY = 50;
|
||||
this.farmSize = 8; // 8x8 tiles (initial)
|
||||
|
||||
// EXPANSION SYSTEM
|
||||
this.unlockedTiles = new Set(); // Tracks unlocked tiles
|
||||
this.expansionCost = 50; // Gold per 2x2 expansion
|
||||
|
||||
// LAND TYPES
|
||||
this.landTypes = {
|
||||
GRASS: 'grass', // Free to use
|
||||
FOREST: 'forest', // Needs clearing (trees)
|
||||
ROCKY: 'rocky', // Needs mining (rocks)
|
||||
SWAMP: 'swamp' // Needs drainage (water)
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
console.log('🌱 MicroFarmSystem initialized');
|
||||
|
||||
// Unlock initial 8x8 farm
|
||||
this.unlockInitialFarm();
|
||||
|
||||
// Create visual boundaries
|
||||
this.createFarmBoundaries();
|
||||
|
||||
// Render locked tile overlay
|
||||
this.renderLockedTileOverlay();
|
||||
|
||||
// Create expansion UI buttons
|
||||
this.createExpansionUI();
|
||||
}
|
||||
|
||||
createExpansionUI() {
|
||||
// Create UI buttons for farm expansion
|
||||
const uiScene = this.scene.scene.get('UIScene');
|
||||
if (!uiScene) {
|
||||
console.warn('⚠️ UIScene not found - cannot create expansion UI');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store reference
|
||||
this.uiScene = uiScene;
|
||||
this.expansionButtons = [];
|
||||
|
||||
const buttonSize = 40;
|
||||
const buttonColor = 0x8B4513; // Brown
|
||||
const buttonHoverColor = 0xD2691E;
|
||||
const expandCost = this.expansionCost;
|
||||
|
||||
// Button positions relative to farm center
|
||||
const tileSize = (this.scene.terrainSystem && this.scene.terrainSystem.tileSize) || 48;
|
||||
const farmWorldX = this.farmCenterX * tileSize;
|
||||
const farmWorldY = this.farmCenterY * tileSize;
|
||||
const farmPixelSize = this.farmSize * tileSize;
|
||||
const halfSize = farmPixelSize / 2;
|
||||
|
||||
const buttons = [
|
||||
{ dir: 'north', x: farmWorldX, y: farmWorldY - halfSize - 60, icon: '⬆️' },
|
||||
{ dir: 'south', x: farmWorldX, y: farmWorldY + halfSize + 60, icon: '⬇️' },
|
||||
{ dir: 'east', x: farmWorldX + halfSize + 60, y: farmWorldY, icon: '➡️' },
|
||||
{ dir: 'west', x: farmWorldX - halfSize - 60, y: farmWorldY, icon: '⬅️' }
|
||||
];
|
||||
|
||||
buttons.forEach(btn => {
|
||||
// Button background
|
||||
const bg = this.scene.add.rectangle(btn.x, btn.y, buttonSize, buttonSize, buttonColor);
|
||||
bg.setStrokeStyle(2, 0xFFFFFF);
|
||||
bg.setDepth(10);
|
||||
bg.setInteractive({ useHandCursor: true });
|
||||
|
||||
// Button text
|
||||
const text = this.scene.add.text(btn.x, btn.y, btn.icon, {
|
||||
fontSize: '20px',
|
||||
color: '#ffffff'
|
||||
}).setOrigin(0.5);
|
||||
text.setDepth(11);
|
||||
|
||||
// Cost label
|
||||
const costLabel = this.scene.add.text(btn.x, btn.y + 30, `${expandCost}g`, {
|
||||
fontSize: '12px',
|
||||
color: '#FFD700',
|
||||
fontStyle: 'bold'
|
||||
}).setOrigin(0.5);
|
||||
costLabel.setDepth(11);
|
||||
|
||||
// Hover effects
|
||||
bg.on('pointerover', () => {
|
||||
bg.setFillStyle(buttonHoverColor);
|
||||
bg.setScale(1.1);
|
||||
});
|
||||
|
||||
bg.on('pointerout', () => {
|
||||
bg.setFillStyle(buttonColor);
|
||||
bg.setScale(1.0);
|
||||
});
|
||||
|
||||
// Click handler
|
||||
bg.on('pointerdown', () => {
|
||||
this.tryExpandFarm(btn.dir);
|
||||
});
|
||||
|
||||
this.expansionButtons.push({ bg, text, costLabel, dir: btn.dir });
|
||||
});
|
||||
|
||||
console.log('✅ Expansion UI created!');
|
||||
}
|
||||
|
||||
tryExpandFarm(direction) {
|
||||
// Check if player has enough gold
|
||||
const inv = this.scene.inventorySystem;
|
||||
if (!inv || inv.gold < this.expansionCost) {
|
||||
console.log(`❌ Not enough gold! Need ${this.expansionCost}g`);
|
||||
|
||||
if (this.scene.events) {
|
||||
this.scene.events.emit('show-floating-text', {
|
||||
x: this.scene.cameras.main.width / 2,
|
||||
y: 100,
|
||||
text: `Need ${this.expansionCost} gold!`,
|
||||
color: '#ff0000'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduct gold
|
||||
inv.gold -= this.expansionCost;
|
||||
|
||||
// Expand farm
|
||||
this.expandFarm(direction);
|
||||
|
||||
// Success feedback
|
||||
if (this.scene.events) {
|
||||
this.scene.events.emit('show-floating-text', {
|
||||
x: this.scene.cameras.main.width / 2,
|
||||
y: 100,
|
||||
text: `✅ Farm expanded ${direction.toUpperCase()}!`,
|
||||
color: '#00ff00'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ Farm expanded ${direction}! (-${this.expansionCost}g)`);
|
||||
}
|
||||
|
||||
unlockInitialFarm() {
|
||||
// Unlock central 8x8 tiles
|
||||
const halfSize = Math.floor(this.farmSize / 2);
|
||||
|
||||
for (let y = this.farmCenterY - halfSize; y < this.farmCenterY + halfSize; y++) {
|
||||
for (let x = this.farmCenterX - halfSize; x < this.farmCenterX + halfSize; x++) {
|
||||
const tileKey = `${x},${y}`;
|
||||
this.unlockedTiles.add(tileKey);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Unlocked ${this.unlockedTiles.size} tiles (8x8 micro farm)`);
|
||||
}
|
||||
|
||||
createFarmBoundaries() {
|
||||
// Clear previous if exists
|
||||
if (this.boundaryGraphics) {
|
||||
this.boundaryGraphics.destroy();
|
||||
}
|
||||
|
||||
const graphics = this.scene.add.graphics();
|
||||
const tileSize = (this.scene.terrainSystem && this.scene.terrainSystem.tileSize) || 48;
|
||||
|
||||
// 🏗️ 1. LARGE MASTER PLATFORM (32x32)
|
||||
const largeSize = 32;
|
||||
const largeHalf = largeSize / 2;
|
||||
const lx1 = (this.farmCenterX - largeHalf) * tileSize;
|
||||
const ly1 = (this.farmCenterY - largeHalf) * tileSize;
|
||||
const lWidth = largeSize * tileSize;
|
||||
const lHeight = largeSize * tileSize;
|
||||
|
||||
// Draw Master Platform
|
||||
graphics.fillStyle(0x0000FF, 0.05); // Very subtle blue for "Large Platform"
|
||||
graphics.fillRect(lx1, ly1, lWidth, lHeight);
|
||||
graphics.lineStyle(2, 0x0000FF, 0.3);
|
||||
graphics.strokeRect(lx1, ly1, lWidth, lHeight);
|
||||
|
||||
// 🏗️ 2. SMALL MICRO FARM PLATFORM (8x8)
|
||||
const halfSize = Math.floor(this.farmSize / 2);
|
||||
const startX = (this.farmCenterX - halfSize) * tileSize;
|
||||
const startY = (this.farmCenterY - halfSize) * tileSize;
|
||||
const width = this.farmSize * tileSize;
|
||||
const height = this.farmSize * tileSize;
|
||||
|
||||
// Draw Highlight
|
||||
graphics.fillStyle(0xFFFFFF, 0.15); // 15% white for "Starter Platform"
|
||||
graphics.fillRect(startX, startY, width, height);
|
||||
|
||||
// Draw Bold Boundary
|
||||
graphics.lineStyle(4, 0x00FF00, 0.8); // High vis green
|
||||
graphics.strokeRect(startX, startY, width, height);
|
||||
|
||||
// Corner markers
|
||||
graphics.fillStyle(0xFFFF00, 1);
|
||||
const markerSize = 12;
|
||||
graphics.fillCircle(startX, startY, markerSize);
|
||||
graphics.fillCircle(startX + width, startY, markerSize);
|
||||
graphics.fillCircle(startX, startY + height, markerSize);
|
||||
graphics.fillCircle(startX + width, startY + height, markerSize);
|
||||
|
||||
graphics.setDepth(50);
|
||||
this.boundaryGraphics = graphics;
|
||||
|
||||
console.log(`🏰 Dual platforms rendered: Master (${largeSize}x${largeSize}) & Micro (${this.farmSize}x${this.farmSize})`);
|
||||
}
|
||||
|
||||
renderLockedTileOverlay() {
|
||||
// Render dark overlay on locked tiles
|
||||
if (this.lockedOverlayGraphics) {
|
||||
this.lockedOverlayGraphics.destroy();
|
||||
}
|
||||
|
||||
this.lockedOverlayGraphics = this.scene.add.graphics();
|
||||
this.lockedOverlayGraphics.setDepth(4); // Above ground, below boundaries
|
||||
|
||||
this.lockedOverlayGraphics.clear();
|
||||
|
||||
// Darken all tiles that are NOT unlocked
|
||||
const halfSize = Math.floor(this.farmSize / 2);
|
||||
const farmStartX = this.farmCenterX - halfSize;
|
||||
const farmEndX = this.farmCenterX + halfSize;
|
||||
const farmStartY = this.farmCenterY - halfSize;
|
||||
const farmEndY = this.farmCenterY + halfSize;
|
||||
|
||||
// Render grid of locked tiles
|
||||
const viewRange = 15; // Show some area around farm
|
||||
for (let y = this.farmCenterY - viewRange; y < this.farmCenterY + viewRange; y++) {
|
||||
for (let x = this.farmCenterX - viewRange; x < this.farmCenterX + viewRange; x++) {
|
||||
// Skip if within farm boundaries
|
||||
if (x >= farmStartX && x < farmEndX && y >= farmStartY && y < farmEndY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Draw dark overlay (lighter)
|
||||
const tileSize = (this.scene.terrainSystem && this.scene.terrainSystem.tileSize) || 48;
|
||||
const worldX = x * tileSize;
|
||||
const worldY = y * tileSize;
|
||||
this.lockedOverlayGraphics.fillStyle(0x000000, 0.3); // 0.5 -> 0.3
|
||||
this.lockedOverlayGraphics.fillRect(worldX, worldY, tileSize, tileSize);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🔒 Locked tile overlay rendered!');
|
||||
}
|
||||
|
||||
isTileUnlocked(tileX, tileY) {
|
||||
const tileKey = `${tileX},${tileY}`;
|
||||
return this.unlockedTiles.has(tileKey);
|
||||
}
|
||||
|
||||
canExpand(direction) {
|
||||
// Check if expansion in direction is possible
|
||||
// direction: 'north', 'south', 'east', 'west'
|
||||
// TODO: Implement expansion logic
|
||||
return true;
|
||||
}
|
||||
|
||||
expandFarm(direction) {
|
||||
// Unlock 2x2 tiles in specified direction
|
||||
const halfSize = Math.floor(this.farmSize / 2);
|
||||
const expansionSize = 2; // Unlock 2x2 tiles at a time
|
||||
|
||||
let newTiles = [];
|
||||
|
||||
switch (direction) {
|
||||
case 'north':
|
||||
for (let y = this.farmCenterY - halfSize - expansionSize; y < this.farmCenterY - halfSize; y++) {
|
||||
for (let x = this.farmCenterX - halfSize; x < this.farmCenterX + halfSize; x++) {
|
||||
newTiles.push({ x, y });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'south':
|
||||
for (let y = this.farmCenterY + halfSize; y < this.farmCenterY + halfSize + expansionSize; y++) {
|
||||
for (let x = this.farmCenterX - halfSize; x < this.farmCenterX + halfSize; x++) {
|
||||
newTiles.push({ x, y });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'east':
|
||||
for (let y = this.farmCenterY - halfSize; y < this.farmCenterY + halfSize; y++) {
|
||||
for (let x = this.farmCenterX + halfSize; x < this.farmCenterX + halfSize + expansionSize; x++) {
|
||||
newTiles.push({ x, y });
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'west':
|
||||
for (let y = this.farmCenterY - halfSize; y < this.farmCenterY + halfSize; y++) {
|
||||
for (let x = this.farmCenterX - halfSize - expansionSize; x < this.farmCenterX - halfSize; x++) {
|
||||
newTiles.push({ x, y });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Unlock the tiles
|
||||
newTiles.forEach(tile => {
|
||||
const tileKey = `${tile.x},${tile.y}`;
|
||||
this.unlockedTiles.add(tileKey);
|
||||
});
|
||||
|
||||
// Update farm size
|
||||
if (direction === 'north' || direction === 'south') {
|
||||
this.farmSize += expansionSize;
|
||||
} else {
|
||||
this.farmSize += expansionSize;
|
||||
}
|
||||
|
||||
// Update visuals
|
||||
this.createFarmBoundaries(); // Redraw boundaries
|
||||
this.renderLockedTileOverlay(); // Update overlay
|
||||
|
||||
console.log(`🔓 Expanded farm ${direction}! +${newTiles.length} tiles. Total: ${this.unlockedTiles.size}`);
|
||||
}
|
||||
|
||||
getLandType(tileX, tileY) {
|
||||
// Determine land type based on tile position
|
||||
// TODO: Use terrain system data
|
||||
|
||||
// For now, return grass for unlocked tiles
|
||||
if (this.isTileUnlocked(tileX, tileY)) {
|
||||
return this.landTypes.GRASS;
|
||||
}
|
||||
|
||||
// Surrounding areas have different types
|
||||
const distFromCenter = Math.sqrt(
|
||||
Math.pow(tileX - this.farmCenterX, 2) +
|
||||
Math.pow(tileY - this.farmCenterY, 2)
|
||||
);
|
||||
|
||||
if (distFromCenter < 10) return this.landTypes.GRASS;
|
||||
if (distFromCenter < 15) return this.landTypes.FOREST;
|
||||
if (distFromCenter < 20) return this.landTypes.ROCKY;
|
||||
return this.landTypes.SWAMP;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Cleanup
|
||||
this.unlockedTiles.clear();
|
||||
}
|
||||
}
|
||||
337
DEBUG_TOTAL_RECOVERY/NPCPopulationSystem.js
Normal file
337
DEBUG_TOTAL_RECOVERY/NPCPopulationSystem.js
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 👥 NPC POPULATION SYSTEM
|
||||
* Spawns and manages NPCs in structures across the world
|
||||
* - Biome-specific NPCs
|
||||
* - Dialog system
|
||||
* - Trading functionality
|
||||
* - Quest giving
|
||||
*/
|
||||
|
||||
class NPCPopulationSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// All NPCs in the world
|
||||
this.npcs = [];
|
||||
|
||||
// NPC types per biome
|
||||
this.npcTypes = {
|
||||
'Grassland': ['farmer', 'blacksmith', 'merchant', 'guard'],
|
||||
'Forest': ['hunter', 'herbalist', 'ranger', 'druid'],
|
||||
'Desert': ['nomad', 'treasure_hunter', 'merchant', 'archaeologist'],
|
||||
'Mountain': ['miner', 'dwarf', 'geologist', 'mountaineer'],
|
||||
'Swamp': ['witch', 'alchemist', 'hermit', 'shaman']
|
||||
};
|
||||
|
||||
// Dialog templates
|
||||
this.dialogs = {
|
||||
'farmer': [
|
||||
"Welcome to my farm! Need any seeds?",
|
||||
"The harvest this year is bountiful!",
|
||||
"I sell the best wheat in the land!"
|
||||
],
|
||||
'merchant': [
|
||||
"Looking to buy or sell? I've got the best deals!",
|
||||
"Welcome, traveler! Check out my wares!",
|
||||
"Gold for goods, goods for gold!"
|
||||
],
|
||||
'hunter': [
|
||||
"The forest is full of game. Happy hunting!",
|
||||
"Watch out for the wolves at night.",
|
||||
"I can sell you some arrows if you need them."
|
||||
],
|
||||
'nomad': [
|
||||
"The desert holds many secrets...",
|
||||
"Water is more valuable than gold here.",
|
||||
"I've traveled far and wide, seen many things."
|
||||
],
|
||||
'miner': [
|
||||
"These mountains are rich with ore!",
|
||||
"I can sell you some iron if you need it.",
|
||||
"Watch your step in those caves!"
|
||||
],
|
||||
'witch': [
|
||||
"Potions and hexes, what do you need?",
|
||||
"The swamp holds ancient magic...",
|
||||
"I can brew you something special."
|
||||
]
|
||||
};
|
||||
|
||||
// Trade goods per NPC type
|
||||
this.tradeGoods = {
|
||||
'farmer': [
|
||||
{ item: 'wheat_seeds', price: 10, stock: 50 },
|
||||
{ item: 'wheat', price: 5, stock: 100 },
|
||||
{ item: 'bread', price: 15, stock: 20 }
|
||||
],
|
||||
'merchant': [
|
||||
{ item: 'wood', price: 8, stock: 200 },
|
||||
{ item: 'stone', price: 6, stock: 150 },
|
||||
{ item: 'iron_ore', price: 20, stock: 50 }
|
||||
],
|
||||
'blacksmith': [
|
||||
{ item: 'iron_sword', price: 150, stock: 5 },
|
||||
{ item: 'iron_pickaxe', price: 100, stock: 10 },
|
||||
{ item: 'iron_axe', price: 120, stock: 8 }
|
||||
],
|
||||
'hunter': [
|
||||
{ item: 'arrow', price: 5, stock: 100 },
|
||||
{ item: 'bow', price: 80, stock: 3 },
|
||||
{ item: 'meat', price: 20, stock: 30 }
|
||||
]
|
||||
};
|
||||
|
||||
console.log('👥 NPCPopulationSystem initialized');
|
||||
}
|
||||
|
||||
// Populate structures with NPCs
|
||||
populateStructures(structureSystem) {
|
||||
if (!structureSystem) return;
|
||||
|
||||
let npcsSpawned = 0;
|
||||
|
||||
// Spawn NPCs in structures (30% chance)
|
||||
for (const structure of structureSystem.structures) {
|
||||
if (Math.random() < 0.3) {
|
||||
const npcType = this.selectNPCType(structure.biome);
|
||||
this.spawnNPC(structure.x, structure.y, npcType, structure.biome);
|
||||
npcsSpawned++;
|
||||
}
|
||||
}
|
||||
|
||||
// Always spawn special NPCs at landmarks
|
||||
for (const landmark of structureSystem.landmarks) {
|
||||
this.spawnNPC(landmark.x, landmark.y, 'quest_giver', landmark.type, true);
|
||||
npcsSpawned++;
|
||||
}
|
||||
|
||||
console.log(`✅ Spawned ${npcsSpawned} NPCs in structures`);
|
||||
}
|
||||
|
||||
// Select NPC type for biome
|
||||
selectNPCType(biome) {
|
||||
const types = this.npcTypes[biome] || this.npcTypes['Grassland'];
|
||||
return types[Math.floor(Math.random() * types.length)];
|
||||
}
|
||||
|
||||
// Spawn single NPC
|
||||
spawnNPC(x, y, type, biome, isQuestGiver = false) {
|
||||
const npc = {
|
||||
x,
|
||||
y,
|
||||
type,
|
||||
biome,
|
||||
isQuestGiver,
|
||||
dialogIndex: 0,
|
||||
hasQuest: isQuestGiver,
|
||||
questCompleted: false,
|
||||
sprite: null
|
||||
};
|
||||
|
||||
this.npcs.push(npc);
|
||||
return npc;
|
||||
}
|
||||
|
||||
// Create NPC sprite (called when chunk loads)
|
||||
createNPCSprite(npc, chunk) {
|
||||
if (npc.sprite) return; // Already has sprite
|
||||
|
||||
const worldX = npc.x * 48 + 24;
|
||||
const worldY = npc.y * 48 + 24;
|
||||
|
||||
// Create simple circle sprite for NPC
|
||||
const color = this.getNPCColor(npc.type);
|
||||
const sprite = this.scene.add.circle(worldX, worldY, 12, color);
|
||||
sprite.setDepth(10 + worldY);
|
||||
|
||||
// Add name label
|
||||
const label = this.scene.add.text(worldX, worldY - 20, npc.type, {
|
||||
fontSize: '12px',
|
||||
color: '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 4, y: 2 }
|
||||
});
|
||||
label.setOrigin(0.5);
|
||||
label.setDepth(10 + worldY);
|
||||
|
||||
// Quest marker for quest givers
|
||||
if (npc.isQuestGiver && !npc.questCompleted) {
|
||||
const questMarker = this.scene.add.text(worldX, worldY - 35, '!', {
|
||||
fontSize: '24px',
|
||||
color: '#FFD700',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
questMarker.setOrigin(0.5);
|
||||
questMarker.setDepth(10 + worldY);
|
||||
|
||||
npc.questMarker = questMarker;
|
||||
if (chunk) chunk.sprites.push(questMarker);
|
||||
}
|
||||
|
||||
npc.sprite = sprite;
|
||||
npc.label = label;
|
||||
|
||||
if (chunk) {
|
||||
chunk.sprites.push(sprite);
|
||||
chunk.sprites.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
// Get NPC color
|
||||
getNPCColor(type) {
|
||||
const colors = {
|
||||
'farmer': 0x8B4513,
|
||||
'merchant': 0xDAA520,
|
||||
'blacksmith': 0x696969,
|
||||
'hunter': 0x228B22,
|
||||
'nomad': 0xD2691E,
|
||||
'miner': 0x708090,
|
||||
'witch': 0x9370DB,
|
||||
'quest_giver': 0xFFD700
|
||||
};
|
||||
return colors[type] || 0x808080;
|
||||
}
|
||||
|
||||
// Check for nearby NPCs
|
||||
update(playerX, playerY) {
|
||||
let nearestNPC = null;
|
||||
let minDist = 3;
|
||||
|
||||
for (const npc of this.npcs) {
|
||||
const dist = Math.sqrt((npc.x - playerX) ** 2 + (npc.y - playerY) ** 2);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearestNPC = npc;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearestNPC && !this.currentNPC) {
|
||||
this.showTalkPrompt(nearestNPC);
|
||||
this.currentNPC = nearestNPC;
|
||||
} else if (!nearestNPC && this.currentNPC) {
|
||||
this.hideTalkPrompt();
|
||||
this.currentNPC = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Show prompt to talk
|
||||
showTalkPrompt(npc) {
|
||||
if (this.talkPrompt) return;
|
||||
|
||||
const promptText = npc.isQuestGiver
|
||||
? '💬 Press T to talk (QUEST AVAILABLE)'
|
||||
: `💬 Press T to talk to ${npc.type}`;
|
||||
|
||||
this.talkPrompt = this.scene.add.text(
|
||||
this.scene.cameras.main.centerX,
|
||||
this.scene.cameras.main.height - 100,
|
||||
promptText,
|
||||
{
|
||||
fontSize: '24px',
|
||||
color: npc.isQuestGiver ? '#FFD700' : '#ffffff',
|
||||
backgroundColor: '#000000',
|
||||
padding: { x: 20, y: 10 }
|
||||
}
|
||||
);
|
||||
this.talkPrompt.setOrigin(0.5);
|
||||
this.talkPrompt.setScrollFactor(0);
|
||||
this.talkPrompt.setDepth(10000);
|
||||
}
|
||||
|
||||
// Hide talk prompt
|
||||
hideTalkPrompt() {
|
||||
if (this.talkPrompt) {
|
||||
this.talkPrompt.destroy();
|
||||
this.talkPrompt = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Talk to NPC (T key)
|
||||
talkToNPC() {
|
||||
if (!this.currentNPC) return;
|
||||
|
||||
const npc = this.currentNPC;
|
||||
|
||||
// Get dialog
|
||||
const dialogs = this.dialogs[npc.type] || ["Hello, traveler!"];
|
||||
const dialog = dialogs[npc.dialogIndex % dialogs.length];
|
||||
npc.dialogIndex++;
|
||||
|
||||
// Show dialog box
|
||||
this.showDialog(npc, dialog);
|
||||
}
|
||||
|
||||
// Show dialog UI
|
||||
showDialog(npc, text) {
|
||||
// Close existing dialog
|
||||
if (this.dialogBox) {
|
||||
this.dialogBox.destroy();
|
||||
this.dialogText.destroy();
|
||||
this.dialogBox = null;
|
||||
}
|
||||
|
||||
// Create dialog box
|
||||
const centerX = this.scene.cameras.main.centerX;
|
||||
const centerY = this.scene.cameras.main.height - 150;
|
||||
|
||||
this.dialogBox = this.scene.add.rectangle(
|
||||
centerX, centerY,
|
||||
600, 120,
|
||||
0x000000, 0.8
|
||||
);
|
||||
this.dialogBox.setStrokeStyle(3, 0xFFFFFF);
|
||||
this.dialogBox.setScrollFactor(0);
|
||||
this.dialogBox.setDepth(10001);
|
||||
|
||||
this.dialogText = this.scene.add.text(
|
||||
centerX, centerY - 30,
|
||||
`${npc.type}:\n${text}`,
|
||||
{
|
||||
fontSize: '20px',
|
||||
color: '#ffffff',
|
||||
align: 'center',
|
||||
wordWrap: { width: 550 }
|
||||
}
|
||||
);
|
||||
this.dialogText.setOrigin(0.5, 0);
|
||||
this.dialogText.setScrollFactor(0);
|
||||
this.dialogText.setDepth(10002);
|
||||
|
||||
// Auto-close after 4 seconds
|
||||
this.scene.time.delayedCall(4000, () => {
|
||||
if (this.dialogBox) {
|
||||
this.dialogBox.destroy();
|
||||
this.dialogText.destroy();
|
||||
this.dialogBox = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get statistics
|
||||
getStats() {
|
||||
const byBiome = {};
|
||||
for (const npc of this.npcs) {
|
||||
byBiome[npc.biome] = (byBiome[npc.biome] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
totalNPCs: this.npcs.length,
|
||||
questGivers: this.npcs.filter(n => n.isQuestGiver).length,
|
||||
byBiome
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.hideTalkPrompt();
|
||||
if (this.dialogBox) {
|
||||
this.dialogBox.destroy();
|
||||
this.dialogText.destroy();
|
||||
}
|
||||
this.npcs.forEach(npc => {
|
||||
if (npc.sprite) npc.sprite.destroy();
|
||||
if (npc.label) npc.label.destroy();
|
||||
if (npc.questMarker) npc.questMarker.destroy();
|
||||
});
|
||||
this.npcs = [];
|
||||
}
|
||||
}
|
||||
454
DEBUG_TOTAL_RECOVERY/NPCPrivacySystem.js
Normal file
454
DEBUG_TOTAL_RECOVERY/NPCPrivacySystem.js
Normal file
@@ -0,0 +1,454 @@
|
||||
/**
|
||||
* NPC PRIVACY & HOME DECORATION SYSTEM
|
||||
* Part of: Game Systems Expansion
|
||||
* Created: January 4, 2026
|
||||
*
|
||||
* Features:
|
||||
* - Hobby-based automatic interior generation
|
||||
* - Door lock system (heart-based access)
|
||||
* - NPC home visit mechanics
|
||||
* - Relationship effects from visits
|
||||
* - Privacy violations & consequences
|
||||
*/
|
||||
|
||||
class NPCPrivacySystem {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.player = game.player;
|
||||
|
||||
// Privacy settings
|
||||
this.privacyLevels = {
|
||||
PUBLIC: 0, // Anyone can enter (living room, kitchen)
|
||||
FRIENDLY: 3, // 3+ hearts required
|
||||
PRIVATE: 5, // 5+ hearts required (bedroom)
|
||||
INTIMATE: 8, // 8+ hearts required (special rooms)
|
||||
LOCKED: 10 // 10 hearts or marriage required
|
||||
};
|
||||
|
||||
// Visit tracking
|
||||
this.visitHistory = {};
|
||||
this.lastVisitTimes = {};
|
||||
|
||||
// NPC home decorations (generated based on hobby)
|
||||
this.npcHomes = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate NPC home interior based on hobby
|
||||
*/
|
||||
generateNPCHome(npcId) {
|
||||
const npc = this.game.npcs.get(npcId);
|
||||
if (!npc) return null;
|
||||
|
||||
const hobby = npc.hobby;
|
||||
|
||||
// Base home structure
|
||||
const home = {
|
||||
npcId: npcId,
|
||||
rooms: {
|
||||
living_room: {
|
||||
name: 'Living Room',
|
||||
privacyLevel: this.privacyLevels.PUBLIC,
|
||||
objects: this.generateLivingRoomObjects(hobby)
|
||||
},
|
||||
kitchen: {
|
||||
name: 'Kitchen',
|
||||
privacyLevel: this.privacyLevels.PUBLIC,
|
||||
objects: this.generateKitchenObjects(hobby)
|
||||
},
|
||||
bedroom: {
|
||||
name: 'Bedroom',
|
||||
privacyLevel: this.privacyLevels.PRIVATE,
|
||||
objects: this.generateBedroomObjects(hobby)
|
||||
},
|
||||
hobby_room: {
|
||||
name: this.getHobbyRoomName(hobby),
|
||||
privacyLevel: this.privacyLevels.FRIENDLY,
|
||||
objects: this.generateHobbyRoomObjects(hobby)
|
||||
}
|
||||
},
|
||||
visitCount: 0,
|
||||
lastVisit: null
|
||||
};
|
||||
|
||||
// Store home
|
||||
this.npcHomes[npcId] = home;
|
||||
|
||||
return home;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate living room objects
|
||||
*/
|
||||
generateLivingRoomObjects(hobby) {
|
||||
const base = [
|
||||
'interior_table_small',
|
||||
'interior_bookshelf',
|
||||
'interior_gothic_lantern'
|
||||
];
|
||||
|
||||
// Hobby-specific additions
|
||||
const hobbyAdditions = {
|
||||
fishing: ['mounted_fish', 'fishing_net'],
|
||||
farming: ['grain_sack', 'tool_rack'],
|
||||
cooking: ['recipe_shelf'],
|
||||
reading: ['bookshelf', 'reading_chair'],
|
||||
music: ['guitar_stand', 'music_sheets'],
|
||||
crafting: ['crafting_workshop']
|
||||
};
|
||||
|
||||
return [...base, ...(hobbyAdditions[hobby] || [])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate kitchen objects
|
||||
*/
|
||||
generateKitchenObjects(hobby) {
|
||||
const base = [
|
||||
'interior_kitchen_stove',
|
||||
'interior_kitchen_counter'
|
||||
];
|
||||
|
||||
if (hobby === 'cooking') {
|
||||
base.push(
|
||||
'interior_kitchen_fridge',
|
||||
'interior_kitchen_sink',
|
||||
'interior_recipe_shelf'
|
||||
);
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate bedroom objects
|
||||
*/
|
||||
generateBedroomObjects(hobby) {
|
||||
const base = [
|
||||
'interior_bed_wooden',
|
||||
'interior_wardrobe',
|
||||
'interior_chest_locked'
|
||||
];
|
||||
|
||||
// Personal items based on hobby
|
||||
const hobbyItems = {
|
||||
zombie_worker: ['brain_jar', 'work_uniform'],
|
||||
baker: ['flour_workspace', 'dough_tools'],
|
||||
barber: ['dreadlock_kit', 'styling_tools'],
|
||||
fisherman: ['tackle_box', 'fish_collection']
|
||||
};
|
||||
|
||||
return [...base, ...(hobbyItems[hobby] || [])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate hobby room objects
|
||||
*/
|
||||
generateHobbyRoomObjects(hobby) {
|
||||
const hobbyRooms = {
|
||||
fishing: [
|
||||
'fishing_rod_rack',
|
||||
'bait_storage',
|
||||
'fish_tank',
|
||||
'mounted_trophy_fish'
|
||||
],
|
||||
zombie_worker: [
|
||||
'brain_jar',
|
||||
'work_bench',
|
||||
'tool_storage',
|
||||
'miner_equipment'
|
||||
],
|
||||
baker: [
|
||||
'flour_workspace',
|
||||
'mixing_bowls',
|
||||
'bread_storage',
|
||||
'recipe_collection'
|
||||
],
|
||||
alchemy: [
|
||||
'interior_alchemy_bottle',
|
||||
'interior_brewing_cauldron',
|
||||
'interior_chemistry_set',
|
||||
'interior_potion_shelf'
|
||||
],
|
||||
crafting: [
|
||||
'interior_crafting_workshop',
|
||||
'interior_tool_rack',
|
||||
'material_storage',
|
||||
'blueprint_table'
|
||||
],
|
||||
reading: [
|
||||
'interior_bookshelf',
|
||||
'reading_chair',
|
||||
'ancient_manuscripts',
|
||||
'writing_desk'
|
||||
]
|
||||
};
|
||||
|
||||
return hobbyRooms[hobby] || ['generic_workspace'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hobby room name
|
||||
*/
|
||||
getHobbyRoomName(hobby) {
|
||||
const names = {
|
||||
fishing: 'Fishing Den',
|
||||
zombie_worker: 'Worker\'s Quarters',
|
||||
baker: 'Baking Workshop',
|
||||
alchemy: 'Alchemy Lab',
|
||||
crafting: 'Craft Room',
|
||||
reading: 'Library',
|
||||
music: 'Music Studio',
|
||||
farming: 'Storage Shed'
|
||||
};
|
||||
|
||||
return names[hobby] || 'Hobby Room';
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to enter NPC room
|
||||
*/
|
||||
attemptEntry(npcId, roomId) {
|
||||
const npc = this.game.npcs.get(npcId);
|
||||
if (!npc) {
|
||||
return { success: false, message: 'NPC not found!' };
|
||||
}
|
||||
|
||||
// Generate home if doesn't exist
|
||||
if (!this.npcHomes[npcId]) {
|
||||
this.generateNPCHome(npcId);
|
||||
}
|
||||
|
||||
const home = this.npcHomes[npcId];
|
||||
const room = home.rooms[roomId];
|
||||
|
||||
if (!room) {
|
||||
return { success: false, message: 'Room not found!' };
|
||||
}
|
||||
|
||||
// Check privacy level
|
||||
const playerHearts = this.player.getRelationshipHearts(npcId);
|
||||
const requiredHearts = room.privacyLevel;
|
||||
|
||||
if (playerHearts < requiredHearts) {
|
||||
// Privacy violation!
|
||||
return this.handlePrivacyViolation(npcId, roomId, requiredHearts);
|
||||
}
|
||||
|
||||
// Allowed entry
|
||||
return this.handleSuccessfulEntry(npcId, roomId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle privacy violation
|
||||
*/
|
||||
handlePrivacyViolation(npcId, roomId, requiredHearts) {
|
||||
const npc = this.game.npcs.get(npcId);
|
||||
|
||||
// Relationship penalty
|
||||
const penalty = (requiredHearts - this.player.getRelationshipHearts(npcId)) * 20;
|
||||
npc.addRelationshipPoints(-penalty);
|
||||
|
||||
// NPC reaction
|
||||
const reactions = [
|
||||
"Hey! That's private!",
|
||||
"What are you doing in my room?!",
|
||||
"Get out! This is MY space!",
|
||||
"I can't believe you just walked in here...",
|
||||
"Privacy, please!"
|
||||
];
|
||||
|
||||
const reaction = Phaser.Utils.Array.GetRandom(reactions);
|
||||
|
||||
this.game.showDialogue(npc.name, reaction, {
|
||||
mood: 'angry',
|
||||
animation: 'shocked'
|
||||
});
|
||||
|
||||
this.game.showMessage(
|
||||
`${npc.name} is upset! -${penalty} relationship points`
|
||||
);
|
||||
|
||||
// Player gets kicked out
|
||||
this.game.player.teleportToLocation('outside_' + npcId + '_home');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
privacyViolation: true,
|
||||
penalty: penalty,
|
||||
requiredHearts: requiredHearts,
|
||||
currentHearts: this.player.getRelationshipHearts(npcId)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle successful entry
|
||||
*/
|
||||
handleSuccessfulEntry(npcId, roomId) {
|
||||
const npc = this.game.npcs.get(npcId);
|
||||
const home = this.npcHomes[npcId];
|
||||
|
||||
// Track visit
|
||||
if (!this.visitHistory[npcId]) {
|
||||
this.visitHistory[npcId] = [];
|
||||
}
|
||||
|
||||
const visit = {
|
||||
roomId: roomId,
|
||||
timestamp: this.game.time.currentTime,
|
||||
timeOfDay: this.game.time.getTimeOfDay()
|
||||
};
|
||||
|
||||
this.visitHistory[npcId].push(visit);
|
||||
this.lastVisitTimes[npcId] = this.game.time.currentTime;
|
||||
|
||||
home.visitCount++;
|
||||
home.lastVisit = this.game.time.currentTime;
|
||||
|
||||
// Relationship effects based on visit
|
||||
this.applyVisitEffects(npcId, roomId, visit);
|
||||
|
||||
// Enter room scene
|
||||
this.game.scene.start('NPCRoomScene', {
|
||||
npcId: npcId,
|
||||
roomId: roomId,
|
||||
room: home.rooms[roomId]
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
room: home.rooms[roomId],
|
||||
npc: npc
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply relationship effects from visit
|
||||
*/
|
||||
applyVisitEffects(npcId, roomId, visit) {
|
||||
const npc = this.game.npcs.get(npcId);
|
||||
const timeOfDay = visit.timeOfDay;
|
||||
|
||||
// Visit frequency check
|
||||
const recentVisits = this.visitHistory[npcId].filter(v => {
|
||||
const hoursSince = (this.game.time.currentTime - v.timestamp) / 3600;
|
||||
return hoursSince < 24; // Last 24 hours
|
||||
});
|
||||
|
||||
if (recentVisits.length > 3) {
|
||||
// Visiting TOO much = annoying
|
||||
npc.addRelationshipPoints(-10);
|
||||
this.game.showMessage(
|
||||
`${npc.name} seems a bit annoyed by frequent visits...`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Time of day effects
|
||||
if (timeOfDay === 'night' && roomId === 'bedroom') {
|
||||
// Visiting bedroom at night = awkward
|
||||
npc.addRelationshipPoints(-5);
|
||||
this.game.showDialogue(
|
||||
npc.name,
|
||||
"It's quite late... Is everything okay?",
|
||||
{ mood: 'concerned' }
|
||||
);
|
||||
} else if (timeOfDay === 'morning' && roomId === 'kitchen') {
|
||||
// Morning kitchen visit = breakfast together!
|
||||
npc.addRelationshipPoints(5);
|
||||
this.game.showDialogue(
|
||||
npc.name,
|
||||
"Good morning! Care to join me for breakfast?",
|
||||
{ mood: 'happy' }
|
||||
);
|
||||
} else if (roomId === 'hobby_room') {
|
||||
// Showing interest in hobby = bonus points!
|
||||
npc.addRelationshipPoints(10);
|
||||
this.game.showDialogue(
|
||||
npc.name,
|
||||
"I'm glad you're interested in my hobby!",
|
||||
{ mood: 'excited' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visit statistics for NPC
|
||||
*/
|
||||
getVisitStats(npcId) {
|
||||
const visits = this.visitHistory[npcId] || [];
|
||||
|
||||
// Count visits per room
|
||||
const roomCounts = {};
|
||||
visits.forEach(visit => {
|
||||
roomCounts[visit.roomId] = (roomCounts[visit.roomId] || 0) + 1;
|
||||
});
|
||||
|
||||
// Average visits per day
|
||||
const daysSinceFirstVisit = visits.length > 0
|
||||
? (this.game.time.currentTime - visits[0].timestamp) / 86400
|
||||
: 0;
|
||||
|
||||
const avgVisitsPerDay = daysSinceFirstVisit > 0
|
||||
? visits.length / daysSinceFirstVisit
|
||||
: 0;
|
||||
|
||||
return {
|
||||
totalVisits: visits.length,
|
||||
roomCounts: roomCounts,
|
||||
avgVisitsPerDay: avgVisitsPerDay,
|
||||
lastVisit: this.lastVisitTimes[npcId],
|
||||
favoriteRoom: Object.keys(roomCounts).reduce((a, b) =>
|
||||
roomCounts[a] > roomCounts[b] ? a : b, null)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player can access special room
|
||||
*/
|
||||
canAccessSpecialRoom(npcId, roomId) {
|
||||
const npc = this.game.npcs.get(npcId);
|
||||
if (!npc) return false;
|
||||
|
||||
const home = this.npcHomes[npcId];
|
||||
if (!home) return false;
|
||||
|
||||
const room = home.rooms[roomId];
|
||||
if (!room) return false;
|
||||
|
||||
const playerHearts = this.player.getRelationshipHearts(npcId);
|
||||
|
||||
// Special case: marriage allows LOCKED access
|
||||
if (room.privacyLevel === this.privacyLevels.LOCKED) {
|
||||
return this.player.spouse === npcId;
|
||||
}
|
||||
|
||||
return playerHearts >= room.privacyLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPC home UI data
|
||||
*/
|
||||
getNPCHomeUIData(npcId) {
|
||||
if (!this.npcHomes[npcId]) {
|
||||
this.generateNPCHome(npcId);
|
||||
}
|
||||
|
||||
const home = this.npcHomes[npcId];
|
||||
const npc = this.game.npcs.get(npcId);
|
||||
|
||||
return {
|
||||
npc: npc,
|
||||
home: home,
|
||||
accessibleRooms: Object.keys(home.rooms).filter(roomId =>
|
||||
this.canAccessSpecialRoom(npcId, roomId)
|
||||
),
|
||||
lockedRooms: Object.keys(home.rooms).filter(roomId =>
|
||||
!this.canAccessSpecialRoom(npcId, roomId)
|
||||
),
|
||||
visitStats: this.getVisitStats(npcId)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
341
DEBUG_TOTAL_RECOVERY/NPCSettlementSystem.js
Normal file
341
DEBUG_TOTAL_RECOVERY/NPCSettlementSystem.js
Normal file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* NPC SETTLEMENT SYSTEM (Magic Helpers)
|
||||
* NPCs auto-assist with tasks
|
||||
* Worker efficiency, happiness, housing
|
||||
*/
|
||||
|
||||
export class NPCSettlementSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Settled NPCs
|
||||
this.settledNPCs = new Map();
|
||||
|
||||
// Worker assignments
|
||||
this.assignments = new Map(); // npcId → task
|
||||
|
||||
// NPC stats
|
||||
this.npcHappiness = new Map(); // npcId → happiness (0-100)
|
||||
this.npcEfficiency = new Map(); // npcId → efficiency (0.5-2.0)
|
||||
|
||||
// Housing
|
||||
this.npcHomes = new Map(); // npcId → buildingId
|
||||
this.housingCapacity = 0;
|
||||
|
||||
// Task types
|
||||
this.taskTypes = ['farming', 'building', 'crafting', 'defense', 'gathering'];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Update worker tasks periodically
|
||||
this.scene.time.addEvent({
|
||||
delay: 5000, // Every 5s
|
||||
callback: () => this.updateWorkers(),
|
||||
loop: true
|
||||
});
|
||||
|
||||
// Update happiness hourly
|
||||
this.scene.time.addEvent({
|
||||
delay: 3600000, // 1 hour
|
||||
callback: () => this.updateHappiness(),
|
||||
loop: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle an NPC (they join the town)
|
||||
*/
|
||||
settleNPC(npcId, npcData) {
|
||||
if (this.settledNPCs.has(npcId)) return false;
|
||||
|
||||
//Check housing availability
|
||||
if (this.settledNPCs.size >= this.housingCapacity) {
|
||||
console.warn('No housing available');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.settledNPCs.set(npcId, {
|
||||
id: npcId,
|
||||
name: npcData.name,
|
||||
skills: npcData.skills || [],
|
||||
assignedTask: null,
|
||||
...npcData
|
||||
});
|
||||
|
||||
// Initial stats
|
||||
this.npcHappiness.set(npcId, 75); // Start at 75% happiness
|
||||
this.npcEfficiency.set(npcId, 1.0); // Base efficiency
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`${npcData.name} has joined the settlement!`,
|
||||
'success',
|
||||
{ icon: 'npc_join' }
|
||||
);
|
||||
|
||||
console.log(`🏘️ ${npcData.name} settled in town`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign NPC to task
|
||||
*/
|
||||
assignTask(npcId, taskType, taskData) {
|
||||
const npc = this.settledNPCs.get(npcId);
|
||||
if (!npc) return false;
|
||||
|
||||
// Check if NPC has skill for task
|
||||
if (!npc.skills.includes(taskType)) {
|
||||
console.warn(`${npc.name} lacks skill: ${taskType}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Assign
|
||||
npc.assignedTask = taskType;
|
||||
this.assignments.set(npcId, {
|
||||
type: taskType,
|
||||
data: taskData,
|
||||
startTime: Date.now()
|
||||
});
|
||||
|
||||
console.log(`👷 ${npc.name} assigned to ${taskType}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unassign NPC from task
|
||||
*/
|
||||
unassignTask(npcId) {
|
||||
const npc = this.settledNPCs.get(npcId);
|
||||
if (!npc) return false;
|
||||
|
||||
npc.assignedTask = null;
|
||||
this.assignments.delete(npcId);
|
||||
|
||||
console.log(`${npc.name} unassigned`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all worker tasks
|
||||
*/
|
||||
updateWorkers() {
|
||||
this.assignments.forEach((assignment, npcId) => {
|
||||
const npc = this.settledNPCs.get(npcId);
|
||||
const efficiency = this.npcEfficiency.get(npcId) || 1.0;
|
||||
|
||||
switch (assignment.type) {
|
||||
case 'farming':
|
||||
this.performFarming(npc, efficiency, assignment.data);
|
||||
break;
|
||||
case 'building':
|
||||
this.performBuilding(npc, efficiency, assignment.data);
|
||||
break;
|
||||
case 'crafting':
|
||||
this.performCrafting(npc, efficiency, assignment.data);
|
||||
break;
|
||||
case 'defense':
|
||||
this.performDefense(npc, efficiency);
|
||||
break;
|
||||
case 'gathering':
|
||||
this.performGathering(npc, efficiency);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* WORKER TASKS
|
||||
*/
|
||||
|
||||
performFarming(npc, efficiency, data) {
|
||||
// Auto-tend crops
|
||||
const crops = this.scene.crops || [];
|
||||
const tendsPerCycle = Math.floor(2 * efficiency);
|
||||
|
||||
for (let i = 0; i < tendsPerCycle && i < crops.length; i++) {
|
||||
const crop = crops[i];
|
||||
crop.water?.();
|
||||
crop.fertilize?.();
|
||||
}
|
||||
|
||||
// Chance to auto-harvest
|
||||
if (Math.random() < 0.1 * efficiency) {
|
||||
const harvestable = crops.find(c => c.isHarvestable);
|
||||
if (harvestable) {
|
||||
this.scene.farmingSystem?.harvestCrop(harvestable);
|
||||
console.log(`${npc.name} harvested crop`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
performBuilding(npc, efficiency, data) {
|
||||
// Speed up construction
|
||||
const building = data.buildingId ? this.scene.buildingSystem?.getBuilding(data.buildingId) : null;
|
||||
|
||||
if (building && building.isUnderConstruction) {
|
||||
const progressBonus = 5 * efficiency;
|
||||
building.constructionProgress += progressBonus;
|
||||
|
||||
if (building.constructionProgress >= 100) {
|
||||
this.scene.buildingSystem?.completeConstruction(building.id);
|
||||
console.log(`${npc.name} completed building: ${building.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
performCrafting(npc, efficiency, data) {
|
||||
// Auto-craft items
|
||||
if (data.recipe) {
|
||||
const canCraft = this.scene.craftingSystem?.canCraft(data.recipe);
|
||||
if (canCraft && Math.random() < 0.2 * efficiency) {
|
||||
this.scene.craftingSystem?.craft(data.recipe);
|
||||
console.log(`${npc.name} crafted ${data.recipe}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
performDefense(npc, efficiency) {
|
||||
// Patrol and defend
|
||||
// Increased detection range for raids
|
||||
this.scene.gameState.buffs.raid_detection_range = (this.scene.gameState.buffs.raid_detection_range || 1.0) + (0.1 * efficiency);
|
||||
|
||||
// Auto-repair walls/defenses
|
||||
const defenses = this.scene.defenseSystem?.getAllDefenses() || [];
|
||||
defenses.forEach(defense => {
|
||||
if (defense.health < defense.maxHealth && Math.random() < 0.05 * efficiency) {
|
||||
defense.health = Math.min(defense.maxHealth, defense.health + 10);
|
||||
console.log(`${npc.name} repaired ${defense.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
performGathering(npc, efficiency) {
|
||||
// Auto-gather resources
|
||||
const gatherChance = 0.15 * efficiency;
|
||||
|
||||
if (Math.random() < gatherChance) {
|
||||
const resources = ['wood', 'stone', 'berries', 'herbs'];
|
||||
const resource = Phaser.Utils.Array.GetRandom(resources);
|
||||
const amount = Math.floor(Phaser.Math.Between(1, 3) * efficiency);
|
||||
|
||||
this.scene.inventorySystem?.addItem(resource, amount);
|
||||
console.log(`${npc.name} gathered ${amount} ${resource}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update NPC happiness
|
||||
*/
|
||||
updateHappiness() {
|
||||
this.settledNPCs.forEach((npc, npcId) => {
|
||||
let happiness = this.npcHappiness.get(npcId) || 50;
|
||||
|
||||
// Factors affecting happiness
|
||||
const hasHome = this.npcHomes.has(npcId);
|
||||
const hasTask = npc.assignedTask !== null;
|
||||
const isOverworked = hasTask && this.assignments.get(npcId)?.overworked;
|
||||
|
||||
// Adjustments
|
||||
if (hasHome) happiness += 5;
|
||||
if (hasTask) happiness += 2;
|
||||
if (isOverworked) happiness -= 10;
|
||||
if (!hasHome && this.settledNPCs.size > this.housingCapacity) happiness -= 15;
|
||||
|
||||
// Natural decay
|
||||
happiness -= 1;
|
||||
|
||||
// Clamp
|
||||
happiness = Math.max(0, Math.min(100, happiness));
|
||||
this.npcHappiness.set(npcId, happiness);
|
||||
|
||||
// Update efficiency based on happiness
|
||||
const efficiency = 0.5 + (happiness / 100) * 1.5; // 0.5-2.0
|
||||
this.npcEfficiency.set(npcId, efficiency);
|
||||
|
||||
// Low happiness warning
|
||||
if (happiness < 30) {
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`${npc.name} is unhappy!`,
|
||||
'warning'
|
||||
);
|
||||
console.warn(`${npc.name} happiness: ${happiness}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign NPC to home
|
||||
*/
|
||||
assignHome(npcId, buildingId) {
|
||||
this.npcHomes.set(npcId, buildingId);
|
||||
|
||||
// Happiness boost
|
||||
const happiness = this.npcHappiness.get(npcId) || 50;
|
||||
this.npcHappiness.set(npcId, Math.min(100, happiness + 20));
|
||||
|
||||
console.log(`${this.settledNPCs.get(npcId).name} moved into home`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase housing capacity
|
||||
*/
|
||||
addHousing(capacity) {
|
||||
this.housingCapacity += capacity;
|
||||
console.log(`Housing capacity: ${this.housingCapacity}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settlement statistics
|
||||
*/
|
||||
getSettlementStats() {
|
||||
const npcs = Array.from(this.settledNPCs.values());
|
||||
const avgHappiness = npcs.reduce((sum, npc) => sum + (this.npcHappiness.get(npc.id) || 0), 0) / npcs.length || 0;
|
||||
const avgEfficiency = npcs.reduce((sum, npc) => sum + (this.npcEfficiency.get(npc.id) || 1), 0) / npcs.length || 1;
|
||||
|
||||
return {
|
||||
population: npcs.length,
|
||||
housingCapacity: this.housingCapacity,
|
||||
averageHappiness: Math.round(avgHappiness),
|
||||
averageEfficiency: avgEfficiency.toFixed(2),
|
||||
assignedWorkers: this.assignments.size,
|
||||
unemployed: npcs.length - this.assignments.size
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPCs by skill
|
||||
*/
|
||||
getNPCsBySkill(skill) {
|
||||
return Array.from(this.settledNPCs.values()).filter(npc => npc.skills.includes(skill));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save/Load
|
||||
*/
|
||||
getSaveData() {
|
||||
return {
|
||||
settledNPCs: Array.from(this.settledNPCs.entries()),
|
||||
assignments: Array.from(this.assignments.entries()),
|
||||
npcHappiness: Array.from(this.npcHappiness.entries()),
|
||||
npcHomes: Array.from(this.npcHomes.entries()),
|
||||
housingCapacity: this.housingCapacity
|
||||
};
|
||||
}
|
||||
|
||||
loadSaveData(data) {
|
||||
this.settledNPCs = new Map(data.settledNPCs || []);
|
||||
this.assignments = new Map(data.assignments || []);
|
||||
this.npcHappiness = new Map(data.npcHappiness || []);
|
||||
this.npcHomes = new Map(data.npcHomes || []);
|
||||
this.housingCapacity = data.housingCapacity || 0;
|
||||
|
||||
// Recalculate efficiency
|
||||
this.npcHappiness.forEach((happiness, npcId) => {
|
||||
const efficiency = 0.5 + (happiness / 100) * 1.5;
|
||||
this.npcEfficiency.set(npcId, efficiency);
|
||||
});
|
||||
}
|
||||
}
|
||||
503
DEBUG_TOTAL_RECOVERY/NPCShopSystem.js
Normal file
503
DEBUG_TOTAL_RECOVERY/NPCShopSystem.js
Normal file
@@ -0,0 +1,503 @@
|
||||
/**
|
||||
* NPCShopSystem.js
|
||||
* ================
|
||||
* KRVAVA ŽETEV - NPC Trading & Shop System (Phase 38)
|
||||
*
|
||||
* Features:
|
||||
* - 4 NPC shop types (Blacksmith, Baker, Trader, Healer)
|
||||
* - Shop UI with buy/sell
|
||||
* - Dynamic pricing
|
||||
* - Stock management
|
||||
* - Relationship discounts
|
||||
*
|
||||
* @author NovaFarma Team
|
||||
* @date 2025-12-23
|
||||
*/
|
||||
|
||||
class NPCShopSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Shop registry
|
||||
this.shops = new Map();
|
||||
this.currentShop = null;
|
||||
|
||||
// Shop UI
|
||||
this.shopContainer = null;
|
||||
this.isShopOpen = false;
|
||||
|
||||
// Player inventory reference
|
||||
this.playerInventory = null;
|
||||
this.playerZlatniki = 0;
|
||||
|
||||
console.log('🛒 NPCShopSystem initialized');
|
||||
|
||||
// Register all shops
|
||||
this.registerShops();
|
||||
|
||||
// Create shop UI
|
||||
this.createShopUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all NPC shops
|
||||
*/
|
||||
registerShops() {
|
||||
const shops = [
|
||||
{
|
||||
id: 'blacksmith',
|
||||
name: 'Kovač (Blacksmith)',
|
||||
npc: 'Ivan the Blacksmith',
|
||||
icon: '⚒️',
|
||||
location: { x: 200, y: 200 },
|
||||
inventory: [
|
||||
// Tools
|
||||
{ id: 'iron_axe', name: 'Iron Axe', price: 200, stock: 5, category: 'tools', locked: true },
|
||||
{ id: 'iron_pickaxe', name: 'Iron Pickaxe', price: 200, stock: 5, category: 'tools', locked: true },
|
||||
{ id: 'iron_hoe', name: 'Iron Hoe', price: 150, stock: 5, category: 'tools', locked: true },
|
||||
{ id: 'watering_can', name: 'Watering Can', price: 100, stock: 10, category: 'tools' },
|
||||
|
||||
// Weapons
|
||||
{ id: 'iron_sword', name: 'Iron Sword', price: 500, stock: 3, category: 'weapons', locked: true },
|
||||
{ id: 'steel_sword', name: 'Steel Sword', price: 1000, stock: 2, category: 'weapons', locked: true },
|
||||
{ id: 'crossbow', name: 'Crossbow', price: 800, stock: 2, category: 'weapons', locked: true },
|
||||
|
||||
|
||||
// Armor
|
||||
{ id: 'leather_armor', name: 'Leather Armor', price: 300, stock: 5, category: 'armor' },
|
||||
{ id: 'iron_armor', name: 'Iron Armor', price: 800, stock: 3, category: 'armor' }
|
||||
],
|
||||
buyback: ['iron_ore', 'steel_bar', 'scrap_metal']
|
||||
},
|
||||
{
|
||||
id: 'baker',
|
||||
name: 'Pekarica (Baker)',
|
||||
npc: 'Maria the Baker',
|
||||
icon: '🍞',
|
||||
location: { x: 250, y: 200 },
|
||||
inventory: [
|
||||
// Food
|
||||
{ id: 'bread', name: 'Bread', price: 10, stock: 50, category: 'food' },
|
||||
{ id: 'cheese', name: 'Cheese', price: 20, stock: 30, category: 'food' },
|
||||
{ id: 'apple_pie', name: 'Apple Pie', price: 50, stock: 20, category: 'food' },
|
||||
{ id: 'cake', name: 'Cake', price: 100, stock: 10, category: 'food' },
|
||||
|
||||
// Recipes
|
||||
{ id: 'recipe_cookies', name: 'Cookie Recipe', price: 200, stock: 1, category: 'recipes' },
|
||||
{ id: 'recipe_pizza', name: 'Pizza Recipe', price: 300, stock: 1, category: 'recipes' },
|
||||
|
||||
// Ingredients
|
||||
{ id: 'flour', name: 'Flour', price: 15, stock: 100, category: 'ingredients' },
|
||||
{ id: 'sugar', name: 'Sugar', price: 20, stock: 80, category: 'ingredients' },
|
||||
{ id: 'yeast', name: 'Yeast', price: 10, stock: 50, category: 'ingredients' }
|
||||
],
|
||||
buyback: ['wheat', 'milk', 'eggs', 'berries']
|
||||
},
|
||||
{
|
||||
id: 'trader',
|
||||
name: 'Trgovec (General Trader)',
|
||||
npc: 'Gregor the Trader',
|
||||
icon: '💰',
|
||||
location: { x: 300, y: 200 },
|
||||
inventory: [
|
||||
// Seeds
|
||||
{ id: 'wheat_seeds', name: 'Wheat Seeds', price: 5, stock: 200, category: 'seeds' },
|
||||
{ id: 'corn_seeds', name: 'Corn Seeds', price: 8, stock: 150, category: 'seeds' },
|
||||
{ id: 'tomato_seeds', name: 'Tomato Seeds', price: 10, stock: 100, category: 'seeds' },
|
||||
{ id: 'strawberry_seeds', name: 'Strawberry Seeds', price: 15, stock: 80, category: 'seeds' },
|
||||
{ id: 'cannabis_seeds', name: 'Cannabis Seeds', price: 20, stock: 50, category: 'seeds' },
|
||||
|
||||
// Materials
|
||||
{ id: 'wood', name: 'Wood', price: 10, stock: 500, category: 'materials' },
|
||||
{ id: 'stone', name: 'Stone', price: 15, stock: 300, category: 'materials' },
|
||||
{ id: 'clay', name: 'Clay', price: 20, stock: 200, category: 'materials' },
|
||||
|
||||
// Special
|
||||
{ id: 'saddle', name: 'Saddle', price: 500, stock: 2, category: 'special' },
|
||||
{ id: 'bouquet', name: 'Bouquet', price: 100, stock: 10, category: 'special' },
|
||||
{ id: 'mermaid_pendant', name: 'Mermaid Pendant', price: 5000, stock: 1, category: 'special' }
|
||||
],
|
||||
buyback: ['crops', 'foraged_items', 'fish']
|
||||
},
|
||||
{
|
||||
id: 'healer',
|
||||
name: 'Zdravnik (Healer)',
|
||||
npc: 'Dr. Ana Kovač',
|
||||
icon: '⚕️',
|
||||
location: { x: 350, y: 200 },
|
||||
inventory: [
|
||||
// Potions
|
||||
{ id: 'health_potion', name: 'Health Potion', price: 50, stock: 50, category: 'potions' },
|
||||
{ id: 'stamina_potion', name: 'Stamina Potion', price: 40, stock: 50, category: 'potions' },
|
||||
{ id: 'antidote', name: 'Antidote', price: 30, stock: 30, category: 'potions' },
|
||||
{ id: 'cure_infection', name: 'Cure Infection', price: 200, stock: 10, category: 'potions' },
|
||||
|
||||
// Research
|
||||
{ id: 'cure_research_1', name: 'Cure Research Notes I', price: 1000, stock: 1, category: 'research' },
|
||||
{ id: 'cure_research_2', name: 'Cure Research Notes II', price: 2000, stock: 1, category: 'research' },
|
||||
|
||||
// Medical supplies
|
||||
{ id: 'bandage', name: 'Bandage', price: 15, stock: 100, category: 'medical' },
|
||||
{ id: 'medicine', name: 'Medicine', price: 80, stock: 30, category: 'medical' }
|
||||
],
|
||||
buyback: ['herbs', 'mushrooms', 'zombie_samples', 'cannabis', 'cannabis_buds']
|
||||
}
|
||||
];
|
||||
|
||||
shops.forEach(shop => this.shops.set(shop.id, shop));
|
||||
|
||||
console.log(`✅ Registered ${this.shops.size} NPC shops`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shop UI
|
||||
*/
|
||||
createShopUI() {
|
||||
const width = this.scene.cameras.main.width;
|
||||
const height = this.scene.cameras.main.height;
|
||||
|
||||
// Main container
|
||||
this.shopContainer = this.scene.add.container(width / 2, height / 2);
|
||||
this.shopContainer.setScrollFactor(0);
|
||||
this.shopContainer.setDepth(10000);
|
||||
this.shopContainer.setVisible(false);
|
||||
|
||||
// Background
|
||||
const bg = this.scene.add.rectangle(0, 0, 900, 600, 0x1a1a1a, 0.95);
|
||||
bg.setStrokeStyle(3, 0xFFD700);
|
||||
this.shopContainer.add(bg);
|
||||
|
||||
// Title (will be updated)
|
||||
this.shopTitle = this.scene.add.text(0, -280, '🛒 SHOP', {
|
||||
fontSize: '32px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#FFD700',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
this.shopTitle.setOrigin(0.5);
|
||||
this.shopContainer.add(this.shopTitle);
|
||||
|
||||
// Close button
|
||||
const closeBtn = this.scene.add.text(430, -280, '❌', {
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer'
|
||||
});
|
||||
closeBtn.setInteractive();
|
||||
closeBtn.on('pointerdown', () => this.closeShop());
|
||||
this.shopContainer.add(closeBtn);
|
||||
|
||||
// Player money display
|
||||
this.moneyText = this.scene.add.text(-430, -250, '💰 0 Zlatniki', {
|
||||
fontSize: '18px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#FFD700'
|
||||
});
|
||||
this.shopContainer.add(this.moneyText);
|
||||
|
||||
// Category tabs
|
||||
this.createCategoryTabs();
|
||||
|
||||
// Item list container
|
||||
this.itemListContainer = this.scene.add.container(0, 0);
|
||||
this.shopContainer.add(this.itemListContainer);
|
||||
|
||||
console.log('✅ Shop UI created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create category tabs
|
||||
*/
|
||||
createCategoryTabs() {
|
||||
const categories = ['all', 'tools', 'weapons', 'food', 'seeds', 'potions'];
|
||||
const tabWidth = 120;
|
||||
const startX = -400;
|
||||
const y = -200;
|
||||
|
||||
categories.forEach((category, index) => {
|
||||
const tab = this.scene.add.rectangle(
|
||||
startX + (index * tabWidth),
|
||||
y,
|
||||
110, 40,
|
||||
0x2d2d2d
|
||||
);
|
||||
tab.setStrokeStyle(2, 0x666666);
|
||||
tab.setInteractive();
|
||||
tab.on('pointerdown', () => this.filterByCategory(category));
|
||||
|
||||
const label = this.scene.add.text(
|
||||
startX + (index * tabWidth),
|
||||
y,
|
||||
category.toUpperCase(),
|
||||
{
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#ffffff'
|
||||
}
|
||||
);
|
||||
label.setOrigin(0.5);
|
||||
|
||||
this.shopContainer.add(tab);
|
||||
this.shopContainer.add(label);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open shop
|
||||
*/
|
||||
openShop(shopId) {
|
||||
const shop = this.shops.get(shopId);
|
||||
if (!shop) {
|
||||
console.error(`Shop ${shopId} not found!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.currentShop = shop;
|
||||
this.isShopOpen = true;
|
||||
|
||||
// Update title
|
||||
this.shopTitle.setText(`${shop.icon} ${shop.name}`);
|
||||
|
||||
// Update money
|
||||
this.updateMoneyDisplay();
|
||||
|
||||
// Display items
|
||||
this.displayShopItems(shop.inventory);
|
||||
|
||||
// Show container
|
||||
this.shopContainer.setVisible(true);
|
||||
|
||||
console.log(`🛒 Opened ${shop.name}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close shop
|
||||
*/
|
||||
closeShop() {
|
||||
this.isShopOpen = false;
|
||||
this.currentShop = null;
|
||||
this.shopContainer.setVisible(false);
|
||||
|
||||
console.log('🛒 Shop closed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display shop items
|
||||
*/
|
||||
displayShopItems(items, filter = 'all') {
|
||||
// Clear previous items
|
||||
this.itemListContainer.removeAll(true);
|
||||
|
||||
// Filter items
|
||||
let filteredItems = items;
|
||||
if (filter !== 'all') {
|
||||
filteredItems = items.filter(item => item.category === filter);
|
||||
}
|
||||
|
||||
// Display items (max 10 visible, scrollable)
|
||||
const itemHeight = 50;
|
||||
const startY = -150;
|
||||
|
||||
filteredItems.slice(0, 10).forEach((item, index) => {
|
||||
const y = startY + (index * itemHeight);
|
||||
|
||||
// Item background
|
||||
const itemBg = this.scene.add.rectangle(-400, y, 850, 45, 0x2d2d2d, 0.8);
|
||||
itemBg.setStrokeStyle(1, 0x444444);
|
||||
this.itemListContainer.add(itemBg);
|
||||
|
||||
// Item name
|
||||
const nameText = this.scene.add.text(-380, y - 10, item.name, {
|
||||
fontSize: '16px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#ffffff'
|
||||
});
|
||||
this.itemListContainer.add(nameText);
|
||||
|
||||
// Stock
|
||||
const stockText = this.scene.add.text(-380, y + 10, `Stock: ${item.stock}`, {
|
||||
fontSize: '12px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#888888'
|
||||
});
|
||||
this.itemListContainer.add(stockText);
|
||||
|
||||
// Price
|
||||
const priceText = this.scene.add.text(200, y, `${item.price} Ž`, {
|
||||
fontSize: '18px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#FFD700',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
priceText.setOrigin(0.5);
|
||||
this.itemListContainer.add(priceText);
|
||||
|
||||
// Buy button
|
||||
const isLocked = item.locked && !this.isShopRestored(this.currentShop.id);
|
||||
const buyColor = isLocked ? 0x666666 : 0x228B22;
|
||||
const buyBtn = this.scene.add.rectangle(350, y, 100, 35, buyColor);
|
||||
buyBtn.setStrokeStyle(2, isLocked ? 0x999999 : 0x32CD32);
|
||||
buyBtn.setInteractive();
|
||||
buyBtn.on('pointerdown', () => {
|
||||
if (isLocked) {
|
||||
this.showNotification({
|
||||
title: 'Shop Ruined',
|
||||
text: `Restore the shop to unlock this item!`,
|
||||
icon: '🏚️'
|
||||
});
|
||||
} else {
|
||||
this.buyItem(item);
|
||||
}
|
||||
});
|
||||
this.itemListContainer.add(buyBtn);
|
||||
|
||||
const buyText = this.scene.add.text(350, y, isLocked ? 'LOCKED' : 'BUY', {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#ffffff',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
buyText.setOrigin(0.5);
|
||||
this.itemListContainer.add(buyText);
|
||||
});
|
||||
}
|
||||
|
||||
isShopRestored(shopId) {
|
||||
if (!this.scene.townRestorationSystem) return true; // Fallback if system missing
|
||||
|
||||
// Map shopId to buildingId
|
||||
const mapping = {
|
||||
'blacksmith': 'jakob_shop', // or whichever ID correctly matches TownRestorationSystem
|
||||
'baker': 'lena_bakery',
|
||||
'healer': 'dr_chen_clinic'
|
||||
};
|
||||
|
||||
const buildingId = mapping[shopId];
|
||||
if (!buildingId) return true; // General trader is always open
|
||||
|
||||
const building = this.scene.townRestorationSystem.buildings.get(buildingId);
|
||||
return building ? building.isRestored : false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filter by category
|
||||
*/
|
||||
filterByCategory(category) {
|
||||
if (!this.currentShop) return;
|
||||
this.displayShopItems(this.currentShop.inventory, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Buy item
|
||||
*/
|
||||
buyItem(item) {
|
||||
// Check stock
|
||||
if (item.stock <= 0) {
|
||||
this.showNotification({
|
||||
title: 'Out of Stock',
|
||||
text: `${item.name} is out of stock!`,
|
||||
icon: '📦'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate price with relationship discount
|
||||
const finalPrice = this.calculatePrice(item.price);
|
||||
|
||||
// Check if player can afford
|
||||
if (this.playerZlatniki < finalPrice) {
|
||||
this.showNotification({
|
||||
title: 'Not Enough Money',
|
||||
text: `Need ${finalPrice}Ž to buy ${item.name}!`,
|
||||
icon: '💰'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Purchase!
|
||||
this.playerZlatniki -= finalPrice;
|
||||
item.stock--;
|
||||
|
||||
// TODO: Add item to player inventory
|
||||
console.log(`✅ Purchased: ${item.name} for ${finalPrice}Ž`);
|
||||
|
||||
// Update UI
|
||||
this.updateMoneyDisplay();
|
||||
this.displayShopItems(this.currentShop.inventory);
|
||||
|
||||
this.showNotification({
|
||||
title: 'Purchase Complete!',
|
||||
text: `Bought ${item.name} for ${finalPrice}Ž!`,
|
||||
icon: '✅'
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate price with discounts
|
||||
*/
|
||||
calculatePrice(basePrice) {
|
||||
// TODO: Apply relationship discounts
|
||||
// For now, return base price
|
||||
return basePrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update money display
|
||||
*/
|
||||
updateMoneyDisplay() {
|
||||
this.moneyText.setText(`💰 ${this.playerZlatniki} Zlatniki`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set player money
|
||||
*/
|
||||
setPlayerMoney(amount) {
|
||||
this.playerZlatniki = amount;
|
||||
this.updateMoneyDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shop info
|
||||
*/
|
||||
getShopInfo(shopId) {
|
||||
return this.shops.get(shopId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all shops
|
||||
*/
|
||||
getAllShops() {
|
||||
return Array.from(this.shops.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Restock shop
|
||||
*/
|
||||
restockShop(shopId) {
|
||||
const shop = this.shops.get(shopId);
|
||||
if (!shop) return false;
|
||||
|
||||
shop.inventory.forEach(item => {
|
||||
item.stock = Math.min(item.stock + 5, 100); // Restock +5, max 100
|
||||
});
|
||||
|
||||
console.log(`📦 ${shop.name} restocked!`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Show notification
|
||||
*/
|
||||
showNotification(notification) {
|
||||
console.log(`📢 ${notification.icon} ${notification.title}: ${notification.text}`);
|
||||
|
||||
const ui = this.scene.scene.get('UIScene');
|
||||
if (ui && ui.showNotification) {
|
||||
ui.showNotification(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
DEBUG_TOTAL_RECOVERY/NPCSpawner.js
Normal file
76
DEBUG_TOTAL_RECOVERY/NPCSpawner.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// NPCSpawner.js - Sistem za spawnjanje NPCjev
|
||||
class NPCSpawner {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.spawnInterval = 30000; // 30 sekund
|
||||
this.maxNPCs = 3; // 3 NPCji na 100x100 mapo
|
||||
this.spawnTimer = 0;
|
||||
|
||||
console.log('🧑 NPCSpawner: Initialized');
|
||||
}
|
||||
|
||||
spawnInitialNPCs() {
|
||||
// Spawn 3 NPCs at random locations
|
||||
for (let i = 0; i < this.maxNPCs; i++) {
|
||||
this.spawnRandomNPC();
|
||||
}
|
||||
console.log(`✅ Spawned ${this.maxNPCs} initial NPCs`);
|
||||
}
|
||||
|
||||
spawnRandomNPC() {
|
||||
if (!this.scene.terrainSystem || !this.scene.npcs) return;
|
||||
|
||||
// Random position (avoid farm area 20,20)
|
||||
let x, y, attempts = 0;
|
||||
do {
|
||||
x = Phaser.Math.Between(5, 95);
|
||||
y = Phaser.Math.Between(5, 95);
|
||||
attempts++;
|
||||
} while (this.isTooCloseToFarm(x, y) && attempts < 50);
|
||||
|
||||
// Check if tile is valid
|
||||
const tile = this.scene.terrainSystem.getTile(x, y);
|
||||
if (!tile || tile.type === 'water') return;
|
||||
|
||||
// Check if decoration exists
|
||||
const key = `${x},${y}`;
|
||||
if (this.scene.terrainSystem.decorationsMap.has(key)) return;
|
||||
|
||||
// Spawn NPC
|
||||
const npc = new NPC(
|
||||
this.scene,
|
||||
x,
|
||||
y,
|
||||
this.scene.terrainOffsetX || 0,
|
||||
this.scene.terrainOffsetY || 0,
|
||||
'zombie' // Type
|
||||
);
|
||||
|
||||
// Set to PASSIVE mode (random walk)
|
||||
npc.state = 'PASSIVE';
|
||||
npc.isTamed = false;
|
||||
|
||||
this.scene.npcs.push(npc);
|
||||
console.log(`🧟 Spawned NPC at (${x}, ${y})`);
|
||||
}
|
||||
|
||||
isTooCloseToFarm(x, y) {
|
||||
const farmX = 50; // Farm center (updated from 20,20 to 50,50)
|
||||
const farmY = 50;
|
||||
const farmRadius = 15;
|
||||
|
||||
const dist = Math.sqrt((x - farmX) ** 2 + (y - farmY) ** 2);
|
||||
return dist < farmRadius;
|
||||
}
|
||||
|
||||
update(delta) {
|
||||
// Check if we need to spawn more NPCs
|
||||
if (this.scene.npcs.length < this.maxNPCs) {
|
||||
this.spawnTimer += delta;
|
||||
if (this.spawnTimer >= this.spawnInterval) {
|
||||
this.spawnTimer = 0;
|
||||
this.spawnRandomNPC();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
600
DEBUG_TOTAL_RECOVERY/SmartZombieSystem.js
Normal file
600
DEBUG_TOTAL_RECOVERY/SmartZombieSystem.js
Normal file
@@ -0,0 +1,600 @@
|
||||
/**
|
||||
* SMART ZOMBIE SYSTEM
|
||||
* Manages zombie intelligence levels (Lv1-10), independent AI, follower commands, and team construction.
|
||||
*
|
||||
* Features:
|
||||
* - Lv1-4: Helpers (resource finding, carry materials, warn danger)
|
||||
* - Lv5-7: Assistants (repair assistance +25% speed, material delivery AI)
|
||||
* - Lv8-10: INDEPENDENT AI (build/repair alone, auto-detect damage, teach others)
|
||||
* - Follower System: 10 zombie followers with commands (Stop, Help, Attack, Home)
|
||||
* - Team Construction: Lv10 leader + multi-zombie mega-projects
|
||||
*/
|
||||
class SmartZombieSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Zombie intelligence data
|
||||
this.zombies = new Map(); // zombieId -> zombie data
|
||||
this.followers = []; // Active followers (max 10)
|
||||
this.independentWorkers = []; // Lv8+ zombies working autonomously
|
||||
this.teams = []; // Construction teams (Lv10 leader + team)
|
||||
|
||||
// Commands
|
||||
this.followerCommands = ['STOP', 'HELP', 'ATTACK', 'HOME'];
|
||||
this.currentCommand = 'HELP'; // Default command
|
||||
|
||||
// AI Task Queue
|
||||
this.taskQueue = []; // {type, position, priority, assignedZombie}
|
||||
|
||||
// Intelligence Levels
|
||||
this.intelligenceLevels = {
|
||||
1: { name: 'Curious', abilities: ['ping_resources', 'carry_light'] },
|
||||
2: { name: 'Aware', abilities: ['carry_medium', 'warn_danger'] },
|
||||
3: { name: 'Helper', abilities: ['gather_resources', 'follow_player'] },
|
||||
4: { name: 'Assistant', abilities: ['detect_ores', 'organize_storage'] },
|
||||
5: { name: 'Skilled', abilities: ['repair_assist', 'deliver_materials'] },
|
||||
6: { name: 'Expert', abilities: ['craft_assist', 'defend_farm'] },
|
||||
7: { name: 'Advanced', abilities: ['build_assist', 'teach_lv1_3'] },
|
||||
8: { name: 'INDEPENDENT', abilities: ['build_alone', 'repair_alone', 'auto_detect_damage'] },
|
||||
9: { name: 'MASTER', abilities: ['teach_all', 'lead_team', 'optimize_tasks'] },
|
||||
10: { name: 'GENIUS', abilities: ['mega_projects', 'invent_blueprints', 'immortal_knowledge'] }
|
||||
};
|
||||
|
||||
console.log('🧠 Smart Zombie System initialized!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or upgrade a zombie
|
||||
*/
|
||||
createZombie(name, level = 1, position = { x: 0, y: 0 }) {
|
||||
const zombieId = `zombie_${Date.now()}_${Math.random()}`;
|
||||
|
||||
const zombie = {
|
||||
id: zombieId,
|
||||
name: name,
|
||||
level: Math.min(level, 10),
|
||||
position: position,
|
||||
status: 'idle', // idle, working, following, patrolling
|
||||
currentTask: null,
|
||||
experience: 0,
|
||||
experienceToNext: this.getExpRequired(level),
|
||||
abilities: this.getAbilities(level),
|
||||
inventory: [],
|
||||
loyalty: 50, // 0-100
|
||||
energy: 100, // 0-100
|
||||
sprite: null // Will be set when rendered
|
||||
};
|
||||
|
||||
this.zombies.set(zombieId, zombie);
|
||||
console.log(`🧟 Created ${name} (Lv${level}) - ${this.intelligenceLevels[level].name}`);
|
||||
|
||||
// Auto-assign if Lv8+
|
||||
if (level >= 8) {
|
||||
this.makeIndependent(zombieId);
|
||||
}
|
||||
|
||||
return zombieId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required XP for next level
|
||||
*/
|
||||
getExpRequired(level) {
|
||||
return 100 * level * level; // 100, 400, 900, 1600, etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Get abilities for a level
|
||||
*/
|
||||
getAbilities(level) {
|
||||
const abilities = [];
|
||||
for (let i = 1; i <= level; i++) {
|
||||
abilities.push(...this.intelligenceLevels[i].abilities);
|
||||
}
|
||||
return [...new Set(abilities)]; // Remove duplicates
|
||||
}
|
||||
|
||||
/**
|
||||
* Add experience to a zombie
|
||||
*/
|
||||
addExperience(zombieId, amount) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return;
|
||||
|
||||
zombie.experience += amount;
|
||||
|
||||
// Level up?
|
||||
while (zombie.experience >= zombie.experienceToNext && zombie.level < 10) {
|
||||
zombie.level++;
|
||||
zombie.experience -= zombie.experienceToNext;
|
||||
zombie.experienceToNext = this.getExpRequired(zombie.level + 1);
|
||||
zombie.abilities = this.getAbilities(zombie.level);
|
||||
|
||||
console.log(`⬆️ ${zombie.name} leveled up to Lv${zombie.level}!`);
|
||||
|
||||
this.scene.events.emit('show-floating-text', {
|
||||
x: zombie.position.x,
|
||||
y: zombie.position.y - 50,
|
||||
text: `LEVEL UP! Lv${zombie.level}`,
|
||||
color: '#FFD700'
|
||||
});
|
||||
|
||||
// Auto-assign if reached Lv8
|
||||
if (zombie.level === 8) {
|
||||
this.makeIndependent(zombieId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a zombie independent (Lv8+)
|
||||
*/
|
||||
makeIndependent(zombieId) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie || zombie.level < 8) return false;
|
||||
|
||||
if (!this.independentWorkers.includes(zombieId)) {
|
||||
this.independentWorkers.push(zombieId);
|
||||
zombie.status = 'independent';
|
||||
|
||||
console.log(`🤖 ${zombie.name} is now INDEPENDENT!`);
|
||||
|
||||
this.scene.events.emit('notification', {
|
||||
title: 'Independent Worker!',
|
||||
message: `${zombie.name} can now work autonomously!`,
|
||||
icon: '🤖'
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add zombie to follower group (max 10)
|
||||
*/
|
||||
addFollower(zombieId) {
|
||||
if (this.followers.length >= 10) {
|
||||
console.log('❌ Maximum 10 followers!');
|
||||
return false;
|
||||
}
|
||||
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return false;
|
||||
|
||||
if (!this.followers.includes(zombieId)) {
|
||||
this.followers.push(zombieId);
|
||||
zombie.status = 'following';
|
||||
|
||||
console.log(`➕ ${zombie.name} joined your followers!`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove zombie from followers
|
||||
*/
|
||||
removeFollower(zombieId) {
|
||||
const index = this.followers.indexOf(zombieId);
|
||||
if (index > -1) {
|
||||
this.followers.splice(index, 1);
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (zombie) {
|
||||
zombie.status = 'idle';
|
||||
console.log(`➖ ${zombie.name} left your followers.`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send command to all followers
|
||||
*/
|
||||
commandFollowers(command) {
|
||||
if (!this.followerCommands.includes(command)) {
|
||||
console.log(`❌ Invalid command: ${command}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentCommand = command;
|
||||
|
||||
this.followers.forEach(zombieId => {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return;
|
||||
|
||||
switch (command) {
|
||||
case 'STOP':
|
||||
zombie.status = 'idle';
|
||||
zombie.currentTask = null;
|
||||
break;
|
||||
|
||||
case 'HELP':
|
||||
zombie.status = 'helping';
|
||||
this.assignHelpTask(zombieId);
|
||||
break;
|
||||
|
||||
case 'ATTACK':
|
||||
zombie.status = 'combat';
|
||||
this.assignCombatTask(zombieId);
|
||||
break;
|
||||
|
||||
case 'HOME':
|
||||
zombie.status = 'returning';
|
||||
this.sendHome(zombieId);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`📢 Commanded ${this.followers.length} followers: ${command}`);
|
||||
|
||||
this.scene.events.emit('notification', {
|
||||
title: 'Follower Command',
|
||||
message: `${this.followers.length} zombies: ${command}`,
|
||||
icon: '📢'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign help task (gather, repair, build)
|
||||
*/
|
||||
assignHelpTask(zombieId) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return;
|
||||
|
||||
// Find nearest task from queue
|
||||
const task = this.findNearestTask(zombie.position);
|
||||
if (task) {
|
||||
zombie.currentTask = task;
|
||||
task.assignedZombie = zombieId;
|
||||
console.log(`🛠️ ${zombie.name} assigned to ${task.type}`);
|
||||
} else {
|
||||
// Follow player and gather resources
|
||||
zombie.currentTask = { type: 'gather', target: 'player' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign combat task
|
||||
*/
|
||||
assignCombatTask(zombieId) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return;
|
||||
|
||||
zombie.currentTask = { type: 'defend', target: 'player', radius: 200 };
|
||||
console.log(`⚔️ ${zombie.name} defending!`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send zombie home
|
||||
*/
|
||||
sendHome(zombieId) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return;
|
||||
|
||||
zombie.currentTask = { type: 'return_home', target: { x: 0, y: 0 } };
|
||||
console.log(`🏠 ${zombie.name} returning home.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find nearest task for zombie
|
||||
*/
|
||||
findNearestTask(position) {
|
||||
if (this.taskQueue.length === 0) return null;
|
||||
|
||||
let nearest = null;
|
||||
let minDist = Infinity;
|
||||
|
||||
this.taskQueue.forEach(task => {
|
||||
if (task.assignedZombie) return; // Already assigned
|
||||
|
||||
const dist = Phaser.Math.Distance.Between(
|
||||
position.x, position.y,
|
||||
task.position.x, task.position.y
|
||||
);
|
||||
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearest = task;
|
||||
}
|
||||
});
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add task to queue
|
||||
*/
|
||||
addTask(type, position, priority = 1) {
|
||||
const task = {
|
||||
id: `task_${Date.now()}`,
|
||||
type: type, // 'build', 'repair', 'gather', 'mine'
|
||||
position: position,
|
||||
priority: priority,
|
||||
assignedZombie: null,
|
||||
progress: 0,
|
||||
required: 100
|
||||
};
|
||||
|
||||
this.taskQueue.push(task);
|
||||
|
||||
// Auto-assign to independent workers
|
||||
this.autoAssignIndependentWorkers();
|
||||
|
||||
return task.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-assign tasks to independent workers (Lv8+)
|
||||
*/
|
||||
autoAssignIndependentWorkers() {
|
||||
this.independentWorkers.forEach(zombieId => {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie || zombie.currentTask) return;
|
||||
|
||||
const task = this.findNearestTask(zombie.position);
|
||||
if (task && zombie.abilities.includes('build_alone') || zombie.abilities.includes('repair_alone')) {
|
||||
zombie.currentTask = task;
|
||||
task.assignedZombie = zombieId;
|
||||
|
||||
console.log(`🤖 Independent: ${zombie.name} auto-assigned to ${task.type}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create construction team (Lv10 leader required)
|
||||
*/
|
||||
createTeam(leaderZombieId, memberZombieIds) {
|
||||
const leader = this.zombies.get(leaderZombieId);
|
||||
if (!leader || leader.level < 10) {
|
||||
console.log('❌ Team leader must be Lv10!');
|
||||
return null;
|
||||
}
|
||||
|
||||
const members = memberZombieIds
|
||||
.map(id => this.zombies.get(id))
|
||||
.filter(z => z && z.level >= 5);
|
||||
|
||||
if (members.length === 0) {
|
||||
console.log('❌ Need at least 1 Lv5+ member!');
|
||||
return null;
|
||||
}
|
||||
|
||||
const teamId = `team_${Date.now()}`;
|
||||
const team = {
|
||||
id: teamId,
|
||||
leader: leaderZombieId,
|
||||
members: memberZombieIds,
|
||||
status: 'ready',
|
||||
currentProject: null,
|
||||
efficiency: 1.0 + (members.length * 0.15) // +15% per member
|
||||
};
|
||||
|
||||
this.teams.push(team);
|
||||
|
||||
console.log(`👥 Team created! Leader: ${leader.name}, Members: ${members.length}, Efficiency: ${team.efficiency}x`);
|
||||
|
||||
this.scene.events.emit('notification', {
|
||||
title: 'Team Created!',
|
||||
message: `${leader.name}'s team (${members.length} members) is ready!`,
|
||||
icon: '👥'
|
||||
});
|
||||
|
||||
return teamId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign mega-project to team
|
||||
*/
|
||||
assignMegaProject(teamId, projectType, position) {
|
||||
const team = this.teams.find(t => t.id === teamId);
|
||||
if (!team) return false;
|
||||
|
||||
const project = {
|
||||
type: projectType, // 'mega_barn', 'fortress', 'factory'
|
||||
position: position,
|
||||
progress: 0,
|
||||
required: 1000, // Mega projects take time
|
||||
efficiency: team.efficiency
|
||||
};
|
||||
|
||||
team.currentProject = project;
|
||||
team.status = 'working';
|
||||
|
||||
console.log(`🏗️ Team assigned to ${projectType}! Efficiency: ${team.efficiency}x`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all zombies (called every frame)
|
||||
*/
|
||||
update(delta) {
|
||||
// Update followers
|
||||
this.followers.forEach(zombieId => {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return;
|
||||
|
||||
this.updateZombie(zombie, delta);
|
||||
});
|
||||
|
||||
// Update independent workers
|
||||
this.independentWorkers.forEach(zombieId => {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return;
|
||||
|
||||
this.updateZombie(zombie, delta);
|
||||
});
|
||||
|
||||
// Update teams
|
||||
this.teams.forEach(team => {
|
||||
if (team.status === 'working' && team.currentProject) {
|
||||
team.currentProject.progress += delta * 0.01 * team.efficiency;
|
||||
|
||||
if (team.currentProject.progress >= team.currentProject.required) {
|
||||
this.completeMegaProject(team);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update individual zombie
|
||||
*/
|
||||
updateZombie(zombie, delta) {
|
||||
if (!zombie.currentTask) return;
|
||||
|
||||
const task = zombie.currentTask;
|
||||
|
||||
switch (task.type) {
|
||||
case 'build':
|
||||
case 'repair':
|
||||
this.progressTask(zombie, task, delta);
|
||||
break;
|
||||
|
||||
case 'gather':
|
||||
this.gatherResources(zombie, delta);
|
||||
break;
|
||||
|
||||
case 'defend':
|
||||
this.defendArea(zombie);
|
||||
break;
|
||||
|
||||
case 'return_home':
|
||||
this.moveToHome(zombie);
|
||||
break;
|
||||
}
|
||||
|
||||
// Drain energy
|
||||
zombie.energy = Math.max(0, zombie.energy - delta * 0.001);
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress on a task
|
||||
*/
|
||||
progressTask(zombie, task, delta) {
|
||||
const speedBonus = zombie.level >= 5 ? 1.25 : 1.0;
|
||||
task.progress += delta * 0.02 * speedBonus;
|
||||
|
||||
if (task.progress >= task.required) {
|
||||
this.completeTask(zombie, task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a task
|
||||
*/
|
||||
completeTask(zombie, task) {
|
||||
console.log(`✅ ${zombie.name} completed ${task.type}!`);
|
||||
|
||||
// Award XP
|
||||
this.addExperience(zombie.id, 50 + (task.priority * 10));
|
||||
|
||||
// Remove from queue
|
||||
const index = this.taskQueue.indexOf(task);
|
||||
if (index > -1) {
|
||||
this.taskQueue.splice(index, 1);
|
||||
}
|
||||
|
||||
zombie.currentTask = null;
|
||||
|
||||
// Find next task if independent
|
||||
if (zombie.status === 'independent') {
|
||||
this.autoAssignIndependentWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather resources
|
||||
*/
|
||||
gatherResources(zombie, delta) {
|
||||
// Simple resource gathering simulation
|
||||
if (Math.random() < 0.01) {
|
||||
const resources = ['wood', 'stone', 'iron_ore'];
|
||||
const resource = resources[Math.floor(Math.random() * resources.length)];
|
||||
|
||||
zombie.inventory.push(resource);
|
||||
console.log(`📦 ${zombie.name} gathered ${resource}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defend area around player/target
|
||||
*/
|
||||
defendArea(zombie) {
|
||||
// Detect nearby enemies (placeholder)
|
||||
// In real game, would check for hostile entities
|
||||
}
|
||||
|
||||
/**
|
||||
* Move zombie to home position
|
||||
*/
|
||||
moveToHome(zombie) {
|
||||
const target = zombie.currentTask.target;
|
||||
const dist = Phaser.Math.Distance.Between(
|
||||
zombie.position.x, zombie.position.y,
|
||||
target.x, target.y
|
||||
);
|
||||
|
||||
if (dist < 10) {
|
||||
zombie.status = 'idle';
|
||||
zombie.currentTask = null;
|
||||
console.log(`🏠 ${zombie.name} arrived home.`);
|
||||
} else {
|
||||
// Move toward home (simplified)
|
||||
const angle = Phaser.Math.Angle.Between(
|
||||
zombie.position.x, zombie.position.y,
|
||||
target.x, target.y
|
||||
);
|
||||
zombie.position.x += Math.cos(angle) * 2;
|
||||
zombie.position.y += Math.sin(angle) * 2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete mega-project
|
||||
*/
|
||||
completeMegaProject(team) {
|
||||
console.log(`🏆 Mega-project ${team.currentProject.type} COMPLETE!`);
|
||||
|
||||
const leader = this.zombies.get(team.leader);
|
||||
if (leader) {
|
||||
this.addExperience(leader.id, 500);
|
||||
}
|
||||
|
||||
team.members.forEach(memberId => {
|
||||
this.addExperience(memberId, 200);
|
||||
});
|
||||
|
||||
this.scene.events.emit('notification', {
|
||||
title: 'Mega-Project Complete!',
|
||||
message: `${team.currentProject.type} is finished!`,
|
||||
icon: '🏆'
|
||||
});
|
||||
|
||||
team.currentProject = null;
|
||||
team.status = 'ready';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get zombie stats
|
||||
*/
|
||||
getZombieStats(zombieId) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return null;
|
||||
|
||||
return {
|
||||
name: zombie.name,
|
||||
level: zombie.level,
|
||||
intelligence: this.intelligenceLevels[zombie.level].name,
|
||||
abilities: zombie.abilities,
|
||||
status: zombie.status,
|
||||
energy: Math.round(zombie.energy),
|
||||
loyalty: zombie.loyalty,
|
||||
experience: `${zombie.experience}/${zombie.experienceToNext}`,
|
||||
inventory: zombie.inventory.length
|
||||
};
|
||||
}
|
||||
}
|
||||
460
DEBUG_TOTAL_RECOVERY/TownGrowthSystem.js
Normal file
460
DEBUG_TOTAL_RECOVERY/TownGrowthSystem.js
Normal file
@@ -0,0 +1,460 @@
|
||||
/**
|
||||
* TOWN GROWTH SYSTEM - Population & Expansion
|
||||
* Part of: Game Systems Expansion
|
||||
* Created: January 4, 2026
|
||||
*
|
||||
* Features:
|
||||
* - Dynamic town population (4 → 20 NPCs)
|
||||
* - Town sign with live stats
|
||||
* - Small villages (5-10 scattered across map)
|
||||
* - Population unlock requirements
|
||||
* - Town Services unlock based on population
|
||||
*/
|
||||
|
||||
class TownGrowthSystem {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.player = game.player;
|
||||
|
||||
// Town stats
|
||||
this.townName = 'Dolina Smrti';
|
||||
this.population = 4; // Starting NPCs: Kai, Ana, Gronk, Baker
|
||||
this.maxPopulation = 20;
|
||||
this.populationSlots = [
|
||||
{ index: 1, unlocked: true, npc: 'kai' },
|
||||
{ index: 2, unlocked: true, npc: 'ana' },
|
||||
{ index: 3, unlocked: true, npc: 'gronk' },
|
||||
{ index: 4, unlocked: true, npc: 'baker' },
|
||||
{ index: 5, unlocked: false, npc: null, requirement: { farmLevel: 2 } },
|
||||
{ index: 6, unlocked: false, npc: null, requirement: { money: 10000 } },
|
||||
{ index: 7, unlocked: false, npc: null, requirement: { quest: 'expand_town_1' } },
|
||||
{ index: 8, unlocked: false, npc: null, requirement: { population: 6 } },
|
||||
{ index: 9, unlocked: false, npc: null, requirement: { building: 'bakery' } },
|
||||
{ index: 10, unlocked: false, npc: null, requirement: { building: 'barbershop' } },
|
||||
{ index: 11, unlocked: false, npc: null, requirement: { hearts: 5, npcId: 'any' } },
|
||||
{ index: 12, unlocked: false, npc: null, requirement: { quest: 'expand_town_2' } },
|
||||
{ index: 13, unlocked: false, npc: null, requirement: { zombieWorkers: 5 } },
|
||||
{ index: 14, unlocked: false, npc: null, requirement: { building: 'mine' } },
|
||||
{ index: 15, unlocked: false, npc: null, requirement: { money: 50000 } },
|
||||
{ index: 16, unlocked: false, npc: null, requirement: { quest: 'expand_town_3' } },
|
||||
{ index: 17, unlocked: false, npc: null, requirement: { marriage: true } },
|
||||
{ index: 18, unlocked: false, npc: null, requirement: { population: 15 } },
|
||||
{ index: 19, unlocked: false, npc: null, requirement: { allBuildings: true } },
|
||||
{ index: 20, unlocked: false, npc: null, requirement: { quest: 'town_master' } }
|
||||
];
|
||||
|
||||
// Town sign
|
||||
this.townSign = {
|
||||
location: { x: 400, y: 300 },
|
||||
visible: true,
|
||||
displayMode: 'population' // 'population', 'status', 'full'
|
||||
};
|
||||
|
||||
// Small villages
|
||||
this.villages = this.initializeVillages();
|
||||
|
||||
// Town services (unlock based on population)
|
||||
this.services = {
|
||||
market: { unlocked: false, requiredPopulation: 6 },
|
||||
hospital: { unlocked: false, requiredPopulation: 8 },
|
||||
school: { unlocked: false, requiredPopulation: 10 },
|
||||
bank: { unlocked: false, requiredPopulation: 12 },
|
||||
museum: { unlocked: false, requiredPopulation: 15 },
|
||||
theater: { unlocked: false, requiredPopulation: 18 }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize small villages
|
||||
*/
|
||||
initializeVillages() {
|
||||
return [
|
||||
{
|
||||
id: 'village_north',
|
||||
name: 'Severna Vas',
|
||||
position: { x: 3000, y: 500 },
|
||||
population: 7,
|
||||
discovered: false,
|
||||
npcs: [
|
||||
{ id: 'fisherman', name: 'Old Fisher', hobby: 'fishing' },
|
||||
{ id: 'hunter', name: 'Hunter Dane', hobby: 'hunting' },
|
||||
{ id: 'hermit', name: 'Wise Hermit', hobby: 'meditation' }
|
||||
],
|
||||
specialItems: ['ancient_fishing_rod', 'hunter_bow', 'meditation_mat']
|
||||
},
|
||||
{
|
||||
id: 'village_east',
|
||||
name: 'Vzhodna Stran',
|
||||
position: { x: 5000, y: 2000 },
|
||||
population: 5,
|
||||
discovered: false,
|
||||
npcs: [
|
||||
{ id: 'blacksmith', name: 'Master Smith', hobby: 'forging' },
|
||||
{ id: 'alchemist', name: 'Mysterious Alchemist', hobby: 'alchemy' }
|
||||
],
|
||||
specialItems: ['master_anvil', 'legendary_hammer', 'philosopher_stone']
|
||||
},
|
||||
{
|
||||
id: 'village_south',
|
||||
name: 'Južno Naselje',
|
||||
position: { x: 2500, y: 4500 },
|
||||
population: 6,
|
||||
discovered: false,
|
||||
npcs: [
|
||||
{ id: 'trader', name: 'Traveling Trader', hobby: 'collecting' },
|
||||
{ id: 'musician', name: 'Bard Luka', hobby: 'music' }
|
||||
],
|
||||
specialItems: ['exotic_seeds', 'rare_instruments', 'ancient_map']
|
||||
},
|
||||
{
|
||||
id: 'village_west',
|
||||
name: 'Zahodna Dolina',
|
||||
position: { x: 500, y: 3000 },
|
||||
population: 8,
|
||||
discovered: false,
|
||||
npcs: [
|
||||
{ id: 'chef', name: 'Chef Antonio', hobby: 'cooking' },
|
||||
{ id: 'librarian', name: 'Keeper of Books', hobby: 'reading' },
|
||||
{ id: 'artist', name: 'Painter Ana', hobby: 'painting' }
|
||||
],
|
||||
specialItems: ['master_cookbook', 'ancient_tome', 'rare_pigments']
|
||||
},
|
||||
{
|
||||
id: 'village_mysterious',
|
||||
name: '??? Mystery Village',
|
||||
position: { x: 4000, y: 4000 },
|
||||
population: 10,
|
||||
discovered: false,
|
||||
hidden: true, // Only visible after completing special quest
|
||||
npcs: [
|
||||
{ id: 'time_keeper', name: 'Keeper of Time', hobby: 'timekeeping' },
|
||||
{ id: 'oracle', name: 'Oracle of Dolina', hobby: 'prophecy' }
|
||||
],
|
||||
specialItems: ['time_crystal', 'prophecy_scroll', 'reality_gem']
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and unlock new population slot
|
||||
*/
|
||||
checkPopulationUnlocks() {
|
||||
let newUnlocks = 0;
|
||||
|
||||
this.populationSlots.forEach(slot => {
|
||||
if (!slot.unlocked && slot.requirement) {
|
||||
if (this.meetsRequirement(slot.requirement)) {
|
||||
slot.unlocked = true;
|
||||
newUnlocks++;
|
||||
|
||||
this.game.showMessage(
|
||||
`New population slot unlocked! (${this.population}/${this.maxPopulation})`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (newUnlocks > 0) {
|
||||
this.updateTownServices();
|
||||
}
|
||||
|
||||
return newUnlocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if requirement is met
|
||||
*/
|
||||
meetsRequirement(requirement) {
|
||||
// Farm level
|
||||
if (requirement.farmLevel) {
|
||||
if (this.player.farmLevel < requirement.farmLevel) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Money
|
||||
if (requirement.money) {
|
||||
if (this.player.money < requirement.money) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Quest
|
||||
if (requirement.quest) {
|
||||
if (!this.player.hasCompletedQuest(requirement.quest)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Building
|
||||
if (requirement.building) {
|
||||
if (!this.player.hasBuilding(requirement.building)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Current population
|
||||
if (requirement.population) {
|
||||
if (this.population < requirement.population) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Zombie workers
|
||||
if (requirement.zombieWorkers) {
|
||||
const zombieCount = this.game.zombieWorkers?.getWorkerCount() || 0;
|
||||
if (zombieCount < requirement.zombieWorkers) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Marriage
|
||||
if (requirement.marriage) {
|
||||
if (!this.player.isMarried) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Hearts with any NPC
|
||||
if (requirement.hearts && requirement.npcId === 'any') {
|
||||
const hasHighRelationship = this.game.npcs.getAllNPCs()
|
||||
.some(npc => npc.relationshipHearts >= requirement.hearts);
|
||||
if (!hasHighRelationship) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// All buildings
|
||||
if (requirement.allBuildings) {
|
||||
const requiredBuildings = ['bakery', 'barbershop', 'lawyer', 'mine', 'hospital'];
|
||||
if (!requiredBuildings.every(b => this.player.hasBuilding(b))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite new NPC to town
|
||||
*/
|
||||
inviteNPC(npcId) {
|
||||
// Find available slot
|
||||
const availableSlot = this.populationSlots.find(
|
||||
slot => slot.unlocked && slot.npc === null
|
||||
);
|
||||
|
||||
if (!availableSlot) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No available population slots!'
|
||||
};
|
||||
}
|
||||
|
||||
// Check if NPC exists
|
||||
const npcData = this.game.npcs.getNPCData(npcId);
|
||||
if (!npcData) {
|
||||
return { success: false, message: 'NPC not found!' };
|
||||
}
|
||||
|
||||
// Assign NPC to slot
|
||||
availableSlot.npc = npcId;
|
||||
|
||||
// Spawn NPC in town
|
||||
this.game.npcs.spawn(npcId, {
|
||||
homeLocation: 'town',
|
||||
moveInDate: this.game.time.currentDate
|
||||
});
|
||||
|
||||
// Increase population
|
||||
this.population++;
|
||||
|
||||
// Update town sign
|
||||
this.updateTownSign();
|
||||
|
||||
// Check for service unlocks
|
||||
this.updateTownServices();
|
||||
|
||||
this.game.showMessage(
|
||||
`${npcData.name} moved to town! Population: ${this.population}/${this.maxPopulation}`
|
||||
);
|
||||
|
||||
return { success: true, npc: npcData };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update town services based on population
|
||||
*/
|
||||
updateTownServices() {
|
||||
let newServices = [];
|
||||
|
||||
Object.entries(this.services).forEach(([serviceId, service]) => {
|
||||
if (!service.unlocked && this.population >= service.requiredPopulation) {
|
||||
service.unlocked = true;
|
||||
newServices.push(serviceId);
|
||||
|
||||
this.game.showMessage(
|
||||
`🏛️ New service unlocked: ${serviceId}! (Pop: ${this.population})`
|
||||
);
|
||||
|
||||
// Trigger service built event
|
||||
this.game.emit('serviceUnlocked', {
|
||||
serviceId: serviceId,
|
||||
population: this.population
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return newServices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update town sign display
|
||||
*/
|
||||
updateTownSign() {
|
||||
const signData = {
|
||||
townName: this.townName,
|
||||
population: this.population,
|
||||
maxPopulation: this.maxPopulation,
|
||||
status: this.getTownStatus(),
|
||||
services: Object.keys(this.services).filter(s => this.services[s].unlocked).length
|
||||
};
|
||||
|
||||
// Emit event to update sign sprite
|
||||
this.game.emit('townSignUpdate', signData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get town status description
|
||||
*/
|
||||
getTownStatus() {
|
||||
if (this.population >= 18) {
|
||||
return 'Thriving City';
|
||||
}
|
||||
if (this.population >= 15) {
|
||||
return 'Prosperous Town';
|
||||
}
|
||||
if (this.population >= 10) {
|
||||
return 'Growing Town';
|
||||
}
|
||||
if (this.population >= 6) {
|
||||
return 'Small Town';
|
||||
}
|
||||
return 'Village';
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover village
|
||||
*/
|
||||
discoverVillage(villageId) {
|
||||
const village = this.villages.find(v => v.id === villageId);
|
||||
|
||||
if (!village) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
if (village.discovered) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Village already discovered!'
|
||||
};
|
||||
}
|
||||
|
||||
// Check if hidden village requires quest
|
||||
if (village.hidden && !this.player.hasCompletedQuest('find_mystery_village')) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'This village remains hidden...'
|
||||
};
|
||||
}
|
||||
|
||||
// Discover village
|
||||
village.discovered = true;
|
||||
|
||||
// Spawn village NPCs
|
||||
village.npcs.forEach(npcData => {
|
||||
this.game.npcs.spawn(npcData.id, {
|
||||
homeLocation: villageId,
|
||||
position: village.position,
|
||||
hobby: npcData.hobby
|
||||
});
|
||||
});
|
||||
|
||||
// Mark special items as available
|
||||
village.specialItems.forEach(itemId => {
|
||||
this.game.items.markAsDiscovered(itemId, villageId);
|
||||
});
|
||||
|
||||
this.game.showMessage(
|
||||
`Discovered ${village.name}! Population: ${village.population} NPCs`
|
||||
);
|
||||
|
||||
// Achievement
|
||||
const discoveredCount = this.villages.filter(v => v.discovered).length;
|
||||
if (discoveredCount === this.villages.length) {
|
||||
this.game.achievements.unlock('village_explorer');
|
||||
}
|
||||
|
||||
return { success: true, village: village };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get travel distance to village
|
||||
*/
|
||||
getTravelDistance(villageId) {
|
||||
const village = this.villages.find(v => v.id === villageId);
|
||||
if (!village) return null;
|
||||
|
||||
const playerPos = this.player.getPosition();
|
||||
const dx = village.position.x - playerPos.x;
|
||||
const dy = village.position.y - playerPos.y;
|
||||
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get village trade options
|
||||
*/
|
||||
getVillageTradeOptions(villageId) {
|
||||
const village = this.villages.find(v => v.id === villageId);
|
||||
if (!village || !village.discovered) return null;
|
||||
|
||||
return {
|
||||
villageName: village.name,
|
||||
npcs: village.npcs,
|
||||
specialItems: village.specialItems,
|
||||
population: village.population
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get town growth UI data
|
||||
*/
|
||||
getTownGrowthUIData() {
|
||||
return {
|
||||
townName: this.townName,
|
||||
population: this.population,
|
||||
maxPopulation: this.maxPopulation,
|
||||
status: this.getTownStatus(),
|
||||
populationSlots: this.populationSlots,
|
||||
availableSlots: this.populationSlots.filter(s => s.unlocked && !s.npc).length,
|
||||
services: this.services,
|
||||
villages: this.villages.filter(v => !v.hidden || v.discovered),
|
||||
discoveredVillages: this.villages.filter(v => v.discovered).length,
|
||||
totalVillages: this.villages.filter(v => !v.hidden).length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update (check for new unlocks)
|
||||
*/
|
||||
update() {
|
||||
// Check for new population slot unlocks
|
||||
this.checkPopulationUnlocks();
|
||||
|
||||
// Update town sign
|
||||
this.updateTownSign();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
474
DEBUG_TOTAL_RECOVERY/TownRestorationLogic.js
Normal file
474
DEBUG_TOTAL_RECOVERY/TownRestorationLogic.js
Normal file
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* TOWN RESTORATION LOGIC SYSTEM
|
||||
* Complete logic for restoring buildings in Mrtva Dolina
|
||||
* Handles material requirements, construction progress, worker assignments, NPC unlocks
|
||||
*/
|
||||
|
||||
export class TownRestorationLogic {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Building states
|
||||
this.buildings = new Map(); //buildingId → buildingData
|
||||
|
||||
// Construction queues
|
||||
this.activeConstructions = [];
|
||||
|
||||
// Worker management
|
||||
this.assignedWorkers = new Map(); // buildingId → [workerIds]
|
||||
|
||||
// NPC unlock tracking
|
||||
this.unlockedNPCs = new Set();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initializeBuildingDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all 14 buildings with restoration data
|
||||
*/
|
||||
initializeBuildingDatabase() {
|
||||
const buildingSpecs = [
|
||||
{
|
||||
id: 'hospital',
|
||||
name: 'Hospital',
|
||||
npcUnlock: 'Ana', // Doctor Ana unlocked
|
||||
materials: {
|
||||
wood: 150,
|
||||
stone: 100,
|
||||
metal: 50,
|
||||
tools: 20
|
||||
},
|
||||
constructionTime: 3600, // 1 hour
|
||||
stages: 3,
|
||||
benefits: ['healing', 'medical_supplies']
|
||||
},
|
||||
{
|
||||
id: 'police',
|
||||
name: 'Police Station',
|
||||
npcUnlock: null,
|
||||
materials: {
|
||||
wood: 100,
|
||||
stone: 150,
|
||||
metal: 80,
|
||||
weapons: 10
|
||||
},
|
||||
constructionTime: 3600,
|
||||
stages: 3,
|
||||
benefits: ['security', 'patrol_unlock']
|
||||
},
|
||||
{
|
||||
id: 'mayor_office',
|
||||
name: "Mayor's Office",
|
||||
npcUnlock: 'Župan', // Mayor unlocked
|
||||
materials: {
|
||||
wood: 120,
|
||||
stone: 80,
|
||||
metal: 40,
|
||||
papers: 50
|
||||
},
|
||||
constructionTime: 2400,
|
||||
stages: 3,
|
||||
benefits: ['election_unlock', 'city_management']
|
||||
},
|
||||
{
|
||||
id: 'tech_workshop',
|
||||
name: 'Tech Workshop',
|
||||
npcUnlock: 'Tehnik', // Technician unlocked
|
||||
materials: {
|
||||
wood: 80,
|
||||
stone: 60,
|
||||
metal: 150,
|
||||
electronics: 30
|
||||
},
|
||||
constructionTime: 3000,
|
||||
stages: 3,
|
||||
benefits: ['tech_upgrades', 'electronics_crafting']
|
||||
},
|
||||
{
|
||||
id: 'tailor',
|
||||
name: 'Tailor Shop',
|
||||
npcUnlock: 'Šivilja', // Seamstress unlocked
|
||||
materials: {
|
||||
wood: 60,
|
||||
fabric: 100,
|
||||
thread: 50,
|
||||
tools: 15
|
||||
},
|
||||
constructionTime: 1800,
|
||||
stages: 3,
|
||||
benefits: ['armor_crafting', 'clothing_upgrades']
|
||||
},
|
||||
{
|
||||
id: 'museum',
|
||||
name: 'Museum',
|
||||
npcUnlock: 'Kustos', // Curator unlocked
|
||||
materials: {
|
||||
wood: 100,
|
||||
stone: 120,
|
||||
glass: 40,
|
||||
artifacts: 10
|
||||
},
|
||||
constructionTime: 4800, // 80 minutes - complex restoration
|
||||
stages: 3,
|
||||
benefits: ['lore_unlock', 'artifact_collection']
|
||||
},
|
||||
{
|
||||
id: 'school',
|
||||
name: 'School',
|
||||
npcUnlock: 'Teacher', // Teacher unlocked
|
||||
materials: {
|
||||
wood: 90,
|
||||
stone: 70,
|
||||
books: 50,
|
||||
tools: 20
|
||||
},
|
||||
constructionTime: 3600,
|
||||
stages: 3,
|
||||
benefits: ['buff_unlock', 'education_system']
|
||||
},
|
||||
{
|
||||
id: 'church',
|
||||
name: 'Church',
|
||||
npcUnlock: 'Župnik', // Priest unlocked
|
||||
materials: {
|
||||
wood: 80,
|
||||
stone: 200,
|
||||
metal: 30,
|
||||
religious_items: 5
|
||||
},
|
||||
constructionTime: 5400, // 90 minutes - sacred restoration
|
||||
stages: 3,
|
||||
benefits: ['blessing_system', 'graveyard_access']
|
||||
},
|
||||
{
|
||||
id: 'blacksmith',
|
||||
name: 'Blacksmith',
|
||||
npcUnlock: 'Ivan Kovač', // Already available
|
||||
materials: {
|
||||
wood: 50,
|
||||
stone: 100,
|
||||
metal: 120,
|
||||
coal: 80
|
||||
},
|
||||
constructionTime: 3000,
|
||||
stages: 3,
|
||||
benefits: ['weapon_crafting', 'tool_upgrades']
|
||||
},
|
||||
{
|
||||
id: 'bakery',
|
||||
name: 'Bakery',
|
||||
npcUnlock: 'Pek', // Baker unlocked
|
||||
materials: {
|
||||
wood: 70,
|
||||
stone: 50,
|
||||
flour: 100,
|
||||
tools: 10
|
||||
},
|
||||
constructionTime: 2400,
|
||||
stages: 3,
|
||||
benefits: ['food_production', 'energy_bonus']
|
||||
}
|
||||
];
|
||||
|
||||
buildingSpecs.forEach(spec => {
|
||||
this.buildings.set(spec.id, {
|
||||
...spec,
|
||||
currentState: 'ruined', // ruined, under_construction, restored
|
||||
currentStage: 0, // 0 = ruined, 1-3 = construction stages
|
||||
progress: 0, // 0-100% construction progress
|
||||
workersAssigned: 0,
|
||||
materialsPaid: false,
|
||||
completionTime: null
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player can start building restoration
|
||||
*/
|
||||
canStartRestoration(buildingId) {
|
||||
const building = this.buildings.get(buildingId);
|
||||
if (!building) return { canStart: false, reason: 'Building not found' };
|
||||
|
||||
if (building.currentState !== 'ruined') {
|
||||
return { canStart: false, reason: 'Building already restored or under construction' };
|
||||
}
|
||||
|
||||
// Check materials
|
||||
const inventory = this.scene.inventorySystem;
|
||||
for (const [material, amount] of Object.entries(building.materials)) {
|
||||
if (!inventory.hasItem(material, amount)) {
|
||||
return {
|
||||
canStart: false,
|
||||
reason: `Not enough ${material}. Need ${amount}, have ${inventory.getItemCount(material)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { canStart: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Start building restoration
|
||||
*/
|
||||
startRestoration(buildingId) {
|
||||
const check = this.canStartRestoration(buildingId);
|
||||
if (!check.canStart) {
|
||||
console.warn(`Cannot start restoration: ${check.reason}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const building = this.buildings.get(buildingId);
|
||||
const inventory = this.scene.inventorySystem;
|
||||
|
||||
// Deduct materials
|
||||
for (const [material, amount] of Object.entries(building.materials)) {
|
||||
inventory.removeItem(material, amount);
|
||||
}
|
||||
|
||||
// Start construction
|
||||
building.currentState = 'under_construction';
|
||||
building.currentStage = 1;
|
||||
building.progress = 0;
|
||||
building.materialsPaid = true;
|
||||
building.completionTime = Date.now() + (building.constructionTime * 1000);
|
||||
|
||||
this.activeConstructions.push(buildingId);
|
||||
|
||||
// Spawn scaffolding visual
|
||||
this.spawnConstructionVisuals(buildingId);
|
||||
|
||||
console.log(`Started restoration of ${building.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign workers to speed up construction
|
||||
*/
|
||||
assignWorker(buildingId, workerType = 'zombie') {
|
||||
const building = this.buildings.get(buildingId);
|
||||
if (!building || building.currentState !== 'under_construction') return false;
|
||||
|
||||
const speedBonus = {
|
||||
'zombie': 0.1, // 10% faster per zombie
|
||||
'human': 0.25, // 25% faster per human NPC
|
||||
'kai': 0.5 // Kai works fastest
|
||||
};
|
||||
|
||||
building.workersAssigned++;
|
||||
|
||||
if (!this.assignedWorkers.has(buildingId)) {
|
||||
this.assignedWorkers.set(buildingId, []);
|
||||
}
|
||||
this.assignedWorkers.get(buildingId).push({ type: workerType, bonus: speedBonus[workerType] });
|
||||
|
||||
console.log(`Assigned ${workerType} to ${building.name}. Speed bonus: +${speedBonus[workerType] * 100}%`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update construction progress
|
||||
*/
|
||||
update(deltaTime) {
|
||||
this.activeConstructions.forEach(buildingId => {
|
||||
const building = this.buildings.get(buildingId);
|
||||
|
||||
// Calculate progress speed (base: 100% over constructionTime)
|
||||
let baseSpeed = (100 / building.constructionTime) * (deltaTime / 1000);
|
||||
|
||||
// Apply worker bonuses
|
||||
if (this.assignedWorkers.has(buildingId)) {
|
||||
const workers = this.assignedWorkers.get(buildingId);
|
||||
const totalBonus = workers.reduce((sum, w) => sum + w.bonus, 0);
|
||||
baseSpeed *= (1 + totalBonus);
|
||||
}
|
||||
|
||||
building.progress += baseSpeed;
|
||||
|
||||
// Check stage transitions
|
||||
if (building.progress >= 33 && building.currentStage === 1) {
|
||||
building.currentStage = 2;
|
||||
this.updateBuildingVisual(buildingId, 2);
|
||||
} else if (building.progress >= 66 && building.currentStage === 2) {
|
||||
building.currentStage = 3;
|
||||
this.updateBuildingVisual(buildingId, 3);
|
||||
}
|
||||
|
||||
// Check completion
|
||||
if (building.progress >= 100) {
|
||||
this.completeRestoration(buildingId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete building restoration
|
||||
*/
|
||||
completeRestoration(buildingId) {
|
||||
const building = this.buildings.get(buildingId);
|
||||
|
||||
building.currentState = 'restored';
|
||||
building.currentStage = 3;
|
||||
building.progress = 100;
|
||||
|
||||
// Remove from active constructions
|
||||
const index = this.activeConstructions.indexOf(buildingId);
|
||||
if (index > -1) this.activeConstructions.splice(index, 1);
|
||||
|
||||
// Remove scaffolding
|
||||
this.removeConstructionVisuals(buildingId);
|
||||
|
||||
// Update building sprite to restored
|
||||
this.updateBuildingVisual(buildingId, 'restored');
|
||||
|
||||
// VFX: Building restoration sparkles
|
||||
this.scene.vfxSystem?.playEffect('building_restoration', building.x, building.y);
|
||||
|
||||
// Unlock NPC if applicable
|
||||
if (building.npcUnlock) {
|
||||
this.unlockNPC(building.npcUnlock);
|
||||
}
|
||||
|
||||
// Grant benefits
|
||||
this.grantBuildingBenefits(buildingId);
|
||||
|
||||
console.log(`✅ ${building.name} restored! NPC unlocked: ${building.npcUnlock || 'None'}`);
|
||||
|
||||
// Quest completion check
|
||||
this.scene.questSystem?.checkBuildingRestoration(buildingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock NPC after building restoration
|
||||
*/
|
||||
unlockNPC(npcName) {
|
||||
if (this.unlockedNPCs.has(npcName)) return;
|
||||
|
||||
this.unlockedNPCs.add(npcName);
|
||||
|
||||
// Spawn NPC in town
|
||||
const npcData = this.getNPCSpawnData(npcName);
|
||||
this.scene.npcSystem?.spawnNPC(npcName, npcData.x, npcData.y);
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification(`${npcName} has arrived in Mrtva Dolina!`, 'success');
|
||||
|
||||
console.log(`🎉 NPC unlocked: ${npcName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPC spawn location based on their building
|
||||
*/
|
||||
getNPCSpawnData(npcName) {
|
||||
const spawnLocations = {
|
||||
'Ana': { x: 1200, y: 800 }, // Hospital
|
||||
'Župan': { x: 1400, y: 700 }, // Mayor's Office
|
||||
'Tehnik': { x: 1100, y: 900 }, // Tech Workshop
|
||||
'Šivilja': { x: 1300, y: 1000 }, // Tailor
|
||||
'Kustos': { x: 1500, y: 600 }, // Museum
|
||||
'Teacher': { x: 1000, y: 700 }, // School
|
||||
'Župnik': { x: 1600, y: 500 }, // Church
|
||||
'Pek': { x: 900, y: 800 } // Bakery
|
||||
};
|
||||
|
||||
return spawnLocations[npcName] || { x: 1200, y: 800 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant building benefits to player
|
||||
*/
|
||||
grantBuildingBenefits(buildingId) {
|
||||
const building = this.buildings.get(buildingId);
|
||||
|
||||
building.benefits.forEach(benefit => {
|
||||
switch (benefit) {
|
||||
case 'healing':
|
||||
this.scene.gameState.unlocks.healing = true;
|
||||
break;
|
||||
case 'security':
|
||||
this.scene.defenseSystem?.unlockPatrols();
|
||||
break;
|
||||
case 'election_unlock':
|
||||
this.scene.electionSystem?.unlockElections();
|
||||
break;
|
||||
case 'tech_upgrades':
|
||||
this.scene.craftingSystem?.unlockCategory('electronics');
|
||||
break;
|
||||
case 'armor_crafting':
|
||||
this.scene.craftingSystem?.unlockCategory('armor');
|
||||
break;
|
||||
case 'lore_unlock':
|
||||
this.scene.gameState.unlocks.museum = true;
|
||||
break;
|
||||
case 'buff_unlock':
|
||||
this.scene.schoolSystem?.enable();
|
||||
break;
|
||||
case 'blessing_system':
|
||||
this.scene.churchSystem?.enable();
|
||||
break;
|
||||
case 'weapon_crafting':
|
||||
this.scene.craftingSystem?.unlockCategory('weapons');
|
||||
break;
|
||||
case 'food_production':
|
||||
this.scene.gameState.unlocks.bakery = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual updates
|
||||
*/
|
||||
spawnConstructionVisuals(buildingId) {
|
||||
// Spawn scaffolding sprite
|
||||
// Spawn worker NPCs/zombies
|
||||
// Add construction sounds
|
||||
}
|
||||
|
||||
removeConstructionVisuals(buildingId) {
|
||||
// Remove scaffolding
|
||||
// Remove workers
|
||||
// Stop construction sounds
|
||||
}
|
||||
|
||||
updateBuildingVisual(buildingId, stageOrState) {
|
||||
// Update building sprite to show construction progress or restored state
|
||||
// stages: 1 (10-30% built), 2 (30-70% built), 3 (70-100% built)
|
||||
// 'restored' = final completed building
|
||||
}
|
||||
|
||||
/**
|
||||
* Get building restoration status
|
||||
*/
|
||||
getBuildingStatus(buildingId) {
|
||||
return this.buildings.get(buildingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all restorable buildings
|
||||
*/
|
||||
getAllBuildings() {
|
||||
return Array.from(this.buildings.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restoration progress summary
|
||||
*/
|
||||
getRestorationProgress() {
|
||||
const total = this.buildings.size;
|
||||
const restored = Array.from(this.buildings.values()).filter(b => b.currentState === 'restored').length;
|
||||
const underConstruction = this.activeConstructions.length;
|
||||
|
||||
return {
|
||||
total,
|
||||
restored,
|
||||
underConstruction,
|
||||
ruined: total - restored - underConstruction,
|
||||
percentComplete: Math.round((restored / total) * 100)
|
||||
};
|
||||
}
|
||||
}
|
||||
403
DEBUG_TOTAL_RECOVERY/TownRestorationSystem.js
Normal file
403
DEBUG_TOTAL_RECOVERY/TownRestorationSystem.js
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* TownRestorationSystem.js
|
||||
* =========================
|
||||
* KRVAVA ŽETEV - Complete Town Restoration System (Phase 19)
|
||||
*
|
||||
* Features:
|
||||
* - 27 towns across 18 biomes
|
||||
* - 150+ ruined buildings
|
||||
* - 180 total NPCs
|
||||
* - Building restoration mechanics
|
||||
* - NPC move-in system
|
||||
* - Global milestones & rewards
|
||||
* - Major city endgame project
|
||||
*
|
||||
* @author NovaFarma Team
|
||||
* @date 2025-12-23
|
||||
*/
|
||||
|
||||
class TownRestorationSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Town registry
|
||||
this.towns = new Map();
|
||||
this.buildings = new Map();
|
||||
this.npcs = new Map();
|
||||
|
||||
// Progress tracking
|
||||
this.buildingsRestored = 0;
|
||||
this.totalBuildings = 150;
|
||||
this.npcsRescued = 0;
|
||||
this.totalNPCs = 180;
|
||||
this.townsCompleted = 0;
|
||||
this.totalTowns = 27;
|
||||
|
||||
// Milestones
|
||||
this.milestones = [10, 25, 50, 100, 180];
|
||||
this.unlockedMilestones = [];
|
||||
|
||||
console.log('🏗️ TownRestorationSystem initialized');
|
||||
|
||||
// Register all towns
|
||||
this.registerTowns();
|
||||
|
||||
// Register Hope Valley buildings
|
||||
this.registerHopeValleyBuildings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all towns
|
||||
*/
|
||||
registerTowns() {
|
||||
// Hope Valley (Starting town - 15 buildings)
|
||||
this.towns.set('hope_valley', {
|
||||
id: 'hope_valley',
|
||||
name: 'Hope Valley',
|
||||
biome: 'grassland',
|
||||
icon: '🏘️',
|
||||
totalBuildings: 15,
|
||||
restoredBuildings: 0,
|
||||
npcs: 0,
|
||||
isUnlocked: true,
|
||||
isCompleted: false
|
||||
});
|
||||
|
||||
// Other towns (6 more buildings each = 156 total)
|
||||
const otherTowns = [
|
||||
{ id: 'forest_grove', name: 'Forest Grove', biome: 'forest', buildings: 6 },
|
||||
{ id: 'desert_oasis', name: 'Desert Oasis', biome: 'desert', buildings: 6 },
|
||||
{ id: 'frozen_harbor', name: 'Frozen Harbor', biome: 'frozen', buildings: 6 },
|
||||
{ id: 'volcanic_refuge', name: 'Volcanic Refuge', biome: 'volcanic', buildings: 6 },
|
||||
{ id: 'coastal_bay', name: 'Coastal Bay', biome: 'beach', buildings: 6 },
|
||||
{ id: 'mountain_peak', name: 'Mountain Peak', biome: 'mountain', buildings: 6 },
|
||||
{ id: 'swamp_village', name: 'Swamp Village', biome: 'swamp', buildings: 6 },
|
||||
{ id: 'crystal_city', name: 'Crystal City', biome: 'crystal', buildings: 8 },
|
||||
{ id: 'atlantis', name: 'Atlantis', biome: 'underwater', buildings: 10 },
|
||||
// ... (27 total towns)
|
||||
];
|
||||
|
||||
otherTowns.forEach(town => {
|
||||
this.towns.set(town.id, {
|
||||
id: town.id,
|
||||
name: town.name,
|
||||
biome: town.biome,
|
||||
icon: '🏘️',
|
||||
totalBuildings: town.buildings,
|
||||
restoredBuildings: 0,
|
||||
npcs: 0,
|
||||
isUnlocked: false,
|
||||
isCompleted: false
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`✅ Registered ${this.towns.size} towns`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Hope Valley buildings (15)
|
||||
*/
|
||||
registerHopeValleyBuildings() {
|
||||
const buildings = [
|
||||
// Essential (NPCs' homes)
|
||||
{ id: 'ivan_house', name: "Ivan's House", npc: 'Ivan', materials: { wood: 100, stone: 50 }, time: 3 },
|
||||
{ id: 'marija_house', name: "Marija's House", npc: 'Marija', materials: { wood: 100, stone: 50 }, time: 3 },
|
||||
{ id: 'jakob_shop', name: "Jakob's Shop", npc: 'Jakob', materials: { wood: 150, iron: 30 }, time: 4 },
|
||||
{ id: 'dr_chen_clinic', name: "Dr. Chen's Clinic", npc: 'Dr. Chen', materials: { wood: 200, stone: 100 }, time: 5 },
|
||||
{ id: 'lena_bakery', name: "Lena's Bakery", npc: 'Lena', materials: { wood: 120, stone: 60 }, time: 4 },
|
||||
|
||||
// Community buildings
|
||||
{ id: 'town_hall', name: 'Town Hall', materials: { wood: 500, stone: 300, iron: 100 }, time: 10 },
|
||||
{ id: 'community_center', name: 'Community Center', materials: { wood: 400, stone: 250 }, time: 8 },
|
||||
{ id: 'library', name: 'Library', materials: { wood: 300, stone: 150 }, time: 6 },
|
||||
{ id: 'school', name: 'School', materials: { wood: 350, stone: 200 }, time: 7 },
|
||||
{ id: 'guard_tower', name: 'Guard Tower', materials: { stone: 400, iron: 150 }, time: 8 },
|
||||
|
||||
// Utility buildings
|
||||
{ id: 'warehouse', name: 'Warehouse', materials: { wood: 250, stone: 150 }, time: 5 },
|
||||
{ id: 'water_tower', name: 'Water Tower', materials: { stone: 300, iron: 100 }, time: 6 },
|
||||
{ id: 'power_station', name: 'Power Station', materials: { stone: 400, iron: 200, wire: 50 }, time: 9 },
|
||||
{ id: 'market', name: 'Market Square', materials: { wood: 200, stone: 100 }, time: 5 },
|
||||
{ id: 'fountain', name: 'Town Fountain', materials: { stone: 150, marble: 50 }, time: 4 }
|
||||
];
|
||||
|
||||
buildings.forEach(building => {
|
||||
this.buildings.set(building.id, {
|
||||
...building,
|
||||
town: 'hope_valley',
|
||||
isRestored: false,
|
||||
progress: 0,
|
||||
workers: [],
|
||||
startTime: null
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`✅ Registered ${buildings.length} Hope Valley buildings`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start building restoration
|
||||
*/
|
||||
startRestoration(buildingId, zombieWorkers = []) {
|
||||
const building = this.buildings.get(buildingId);
|
||||
if (!building) {
|
||||
console.error(`Building ${buildingId} not found!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (building.isRestored) {
|
||||
console.log(`${building.name} is already restored!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check materials
|
||||
if (!this.hasMaterials(building.materials)) {
|
||||
console.log(`Not enough materials for ${building.name}!`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Consume materials
|
||||
this.consumeMaterials(building.materials);
|
||||
|
||||
// Assign workers
|
||||
building.workers = zombieWorkers;
|
||||
building.startTime = Date.now();
|
||||
building.progress = 0;
|
||||
|
||||
// Calculate construction time (base time reduced by workers)
|
||||
const workerBonus = zombieWorkers.length * 0.2; // 20% per worker
|
||||
const constructionTime = building.time * (1 - workerBonus);
|
||||
|
||||
console.log(`🏗️ Started restoring ${building.name} (${constructionTime} days with ${zombieWorkers.length} workers)`);
|
||||
|
||||
// Auto-complete after time
|
||||
setTimeout(() => {
|
||||
this.completeRestoration(buildingId);
|
||||
}, constructionTime * 1000 * 60); // Convert to ms (for demo, 1 day = 1 min)
|
||||
|
||||
this.showNotification({
|
||||
title: 'Restoration Started!',
|
||||
text: `🏗️ ${building.name} - ${constructionTime} days`,
|
||||
icon: '🔨'
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete building restoration
|
||||
*/
|
||||
completeRestoration(buildingId) {
|
||||
const building = this.buildings.get(buildingId);
|
||||
if (!building) return;
|
||||
|
||||
building.isRestored = true;
|
||||
building.progress = 100;
|
||||
this.buildingsRestored++;
|
||||
|
||||
console.log(`✅ ${building.name} restored! (${this.buildingsRestored}/${this.totalBuildings})`);
|
||||
|
||||
// Move in NPC if building has one
|
||||
if (building.npc) {
|
||||
this.moveInNPC(building.npc, building.town);
|
||||
}
|
||||
|
||||
// Update town progress
|
||||
this.updateTownProgress(building.town);
|
||||
|
||||
// Check milestones
|
||||
this.checkMilestones();
|
||||
|
||||
// Update visuals in the current scene
|
||||
if (this.scene.updateBuildingVisuals) {
|
||||
this.scene.updateBuildingVisuals(buildingId);
|
||||
}
|
||||
|
||||
this.showNotification({
|
||||
title: 'Building Complete!',
|
||||
text: `✅ ${building.name} restored!`,
|
||||
icon: '🏗️'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Move in NPC
|
||||
*/
|
||||
moveInNPC(npcName, townId) {
|
||||
const npc = {
|
||||
name: npcName,
|
||||
town: townId,
|
||||
movedInDate: Date.now(),
|
||||
hearts: 0,
|
||||
giftsGiven: 0,
|
||||
questsCompleted: 0
|
||||
};
|
||||
|
||||
this.npcs.set(npcName, npc);
|
||||
this.npcsRescued++;
|
||||
|
||||
console.log(`👤 ${npcName} moved into ${townId}! (${this.npcsRescued}/${this.totalNPCs})`);
|
||||
|
||||
// Update town NPC count
|
||||
const town = this.towns.get(townId);
|
||||
if (town) {
|
||||
town.npcs++;
|
||||
}
|
||||
|
||||
this.showNotification({
|
||||
title: 'NPC Moved In!',
|
||||
text: `👤 ${npcName} is now living in town!`,
|
||||
icon: '🏠'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update town progress
|
||||
*/
|
||||
updateTownProgress(townId) {
|
||||
const town = this.towns.get(townId);
|
||||
if (!town) return;
|
||||
|
||||
// Count restored buildings in this town
|
||||
const townBuildings = Array.from(this.buildings.values())
|
||||
.filter(b => b.town === townId);
|
||||
const restored = townBuildings.filter(b => b.isRestored).length;
|
||||
|
||||
town.restoredBuildings = restored;
|
||||
|
||||
// Check if town is complete
|
||||
if (restored === town.totalBuildings) {
|
||||
town.isCompleted = true;
|
||||
this.townsCompleted++;
|
||||
|
||||
console.log(`🏆 ${town.name} 100% COMPLETE! (${this.townsCompleted}/${this.totalTowns})`);
|
||||
|
||||
this.showNotification({
|
||||
title: 'TOWN COMPLETE!',
|
||||
text: `🏆 ${town.name} fully restored!`,
|
||||
icon: '🎉'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check milestones
|
||||
*/
|
||||
checkMilestones() {
|
||||
this.milestones.forEach(milestone => {
|
||||
if (this.npcsRescued >= milestone && !this.unlockedMilestones.includes(milestone)) {
|
||||
this.unlockMilestone(milestone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock milestone
|
||||
*/
|
||||
unlockMilestone(milestone) {
|
||||
this.unlockedMilestones.push(milestone);
|
||||
|
||||
console.log(`🏆 MILESTONE: ${milestone} NPCs rescued!`);
|
||||
|
||||
// Grant rewards
|
||||
const rewards = this.getMilestoneReward(milestone);
|
||||
|
||||
this.showNotification({
|
||||
title: `MILESTONE: ${milestone} NPCs!`,
|
||||
text: `🏆 Unlocked: ${rewards.text}`,
|
||||
icon: '🎉'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get milestone reward
|
||||
*/
|
||||
getMilestoneReward(milestone) {
|
||||
const rewards = {
|
||||
10: { text: 'Community Center unlocked!', feature: 'community_center' },
|
||||
25: { text: 'Town Festivals enabled!', feature: 'festivals' },
|
||||
50: { text: 'Town Guard system!', feature: 'town_guard' },
|
||||
100: { text: 'Major City project unlocked!', feature: 'major_city' },
|
||||
180: { text: 'UTOPIA ENDING unlocked!', feature: 'utopia_ending' }
|
||||
};
|
||||
|
||||
return rewards[milestone] || { text: 'Bonus unlocked!' };
|
||||
}
|
||||
|
||||
hasMaterials(materials) {
|
||||
if (!this.scene.inventorySystem) return true; // Safety fallback
|
||||
|
||||
for (const [item, count] of Object.entries(materials)) {
|
||||
if (!this.scene.inventorySystem.hasItem(item, count)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
consumeMaterials(materials) {
|
||||
if (!this.scene.inventorySystem) return;
|
||||
|
||||
for (const [item, count] of Object.entries(materials)) {
|
||||
this.scene.inventorySystem.removeItem(item, count);
|
||||
}
|
||||
console.log('📦 Materials consumed:', materials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restoration progress
|
||||
*/
|
||||
getProgress() {
|
||||
return {
|
||||
buildings: {
|
||||
restored: this.buildingsRestored,
|
||||
total: this.totalBuildings,
|
||||
percentage: ((this.buildingsRestored / this.totalBuildings) * 100).toFixed(1)
|
||||
},
|
||||
npcs: {
|
||||
rescued: this.npcsRescued,
|
||||
total: this.totalNPCs,
|
||||
percentage: ((this.npcsRescued / this.totalNPCs) * 100).toFixed(1)
|
||||
},
|
||||
towns: {
|
||||
completed: this.townsCompleted,
|
||||
total: this.totalTowns,
|
||||
percentage: ((this.townsCompleted / this.totalTowns) * 100).toFixed(1)
|
||||
},
|
||||
milestones: this.unlockedMilestones
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get town info
|
||||
*/
|
||||
getTownInfo(townId) {
|
||||
return this.towns.get(townId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all towns
|
||||
*/
|
||||
getAllTowns() {
|
||||
return Array.from(this.towns.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get building info
|
||||
*/
|
||||
getBuildingInfo(buildingId) {
|
||||
return this.buildings.get(buildingId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Show notification
|
||||
*/
|
||||
showNotification(notification) {
|
||||
console.log(`📢 ${notification.icon} ${notification.title}: ${notification.text}`);
|
||||
|
||||
const ui = this.scene.scene.get('UIScene');
|
||||
if (ui && ui.showNotification) {
|
||||
ui.showNotification(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
432
DEBUG_TOTAL_RECOVERY/TwinBondSystem.js
Normal file
432
DEBUG_TOTAL_RECOVERY/TwinBondSystem.js
Normal file
@@ -0,0 +1,432 @@
|
||||
/**
|
||||
* TwinBondSystem.js
|
||||
* =================
|
||||
* KRVAVA ŽETEV - Twin Bond Mechanic (Kai ↔ Ana Connection)
|
||||
*
|
||||
* Core Concept:
|
||||
* Kai and Ana share a psychic bond through the Alfa virus
|
||||
* As twins, they can:
|
||||
* - Feel each other's emotions
|
||||
* - Sense each other's location (vaguely)
|
||||
* - Communicate telepathically (limited)
|
||||
* - Share HP/stamina in emergencies
|
||||
*
|
||||
* Features:
|
||||
* - Bond Strength meter (0-100)
|
||||
* - Telepathic messages from Ana
|
||||
* - Direction to Ana indicator
|
||||
* - Twin abilities (heal twin, boost twin, swap positions)
|
||||
* - Bond events (visions, flashbacks)
|
||||
* - Ana's status tracking (health, danger level)
|
||||
*
|
||||
* @author NovaFarma Team
|
||||
* @date 2025-12-23
|
||||
*/
|
||||
|
||||
class TwinBondSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Bond state
|
||||
this.bondStrength = 75; // Starts strong (0-100)
|
||||
this.maxBondStrength = 100;
|
||||
|
||||
// Ana's state (unknown location)
|
||||
this.anaState = {
|
||||
alive: true,
|
||||
health: 100,
|
||||
dangerLevel: 0, // 0 = safe, 100 = critical
|
||||
distance: 5000, // pixels from Kai (initially far)
|
||||
direction: { x: 1000, y: 1000 }, // General direction
|
||||
lastMessage: null,
|
||||
messageTime: null
|
||||
};
|
||||
|
||||
// Bond abilities
|
||||
this.abilities = {
|
||||
telepathy: { unlocked: true, cooldown: 0, maxCooldown: 30000 }, // 30s
|
||||
sensePulse: { unlocked: true, cooldown: 0, maxCooldown: 60000 }, // 1min
|
||||
emergencyLink: { unlocked: false, cooldown: 0, maxCooldown: 300000 }, // 5min
|
||||
twinRecall: { unlocked: false, cooldown: 0, maxCooldown: 600000 } // 10min
|
||||
};
|
||||
|
||||
// Messages from Ana (telepathic)
|
||||
this.messageQueue = [];
|
||||
this.lastMessageTime = 0;
|
||||
|
||||
// UI elements
|
||||
this.bondUI = null;
|
||||
|
||||
// Events
|
||||
this.bondEvents = this.defineBondEvents();
|
||||
this.nextEventTime = Date.now() + 60000; // First event in 1 minute
|
||||
|
||||
console.log('💞 TwinBondSystem initialized - Bond Strength:', this.bondStrength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bond strength based on actions
|
||||
*/
|
||||
update(delta) {
|
||||
const deltaSeconds = delta / 1000;
|
||||
|
||||
// Passive bond decay (small)
|
||||
this.bondStrength = Math.max(0, this.bondStrength - 0.01 * deltaSeconds);
|
||||
|
||||
// Update ability cooldowns
|
||||
for (const ability in this.abilities) {
|
||||
if (this.abilities[ability].cooldown > 0) {
|
||||
this.abilities[ability].cooldown -= delta;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for random bond events
|
||||
if (Date.now() > this.nextEventTime) {
|
||||
this.triggerRandomBondEvent();
|
||||
this.nextEventTime = Date.now() + Phaser.Math.Between(60000, 180000); // 1-3 min
|
||||
}
|
||||
|
||||
// Update Ana's danger level based on story progress
|
||||
this.updateAnaDanger(deltaSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define bond events (telepathic visions)
|
||||
*/
|
||||
defineBondEvents() {
|
||||
return [
|
||||
{
|
||||
id: 'first_contact',
|
||||
trigger: 'auto',
|
||||
condition: null,
|
||||
message: 'Kai... can you hear me? I\'m... somewhere dark...',
|
||||
emotion: 'worried',
|
||||
bondChange: +5
|
||||
},
|
||||
{
|
||||
id: 'danger_warning',
|
||||
trigger: 'danger_high',
|
||||
condition: () => this.anaState.dangerLevel > 70,
|
||||
message: 'Brother! They\'re coming for me! Please hurry!',
|
||||
emotion: 'fear',
|
||||
bondChange: -10
|
||||
},
|
||||
{
|
||||
id: 'memory_flash',
|
||||
trigger: 'random',
|
||||
condition: null,
|
||||
message: 'Remember when we first discovered the Alfa strain? We were so hopeful...',
|
||||
emotion: 'sad',
|
||||
bondChange: +3
|
||||
},
|
||||
{
|
||||
id: 'location_hint',
|
||||
trigger: 'sense_pulse',
|
||||
condition: null,
|
||||
message: 'I can feel concrete walls... cold metal... some kind of facility?',
|
||||
emotion: 'neutral',
|
||||
bondChange: +5
|
||||
},
|
||||
{
|
||||
id: 'encouragement',
|
||||
trigger: 'random',
|
||||
condition: () => this.scene.player?.hp < 50,
|
||||
message: 'Stay strong, Kai! I believe in you!',
|
||||
emotion: 'determined',
|
||||
bondChange: +10
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a random bond event
|
||||
*/
|
||||
triggerRandomBondEvent() {
|
||||
const randomEvents = this.bondEvents.filter(e => e.trigger === 'random');
|
||||
|
||||
if (randomEvents.length === 0) return;
|
||||
|
||||
const event = Phaser.Utils.Array.GetRandom(randomEvents);
|
||||
|
||||
// Check condition
|
||||
if (event.condition && !event.condition()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showTelepathicMessage(event.message, event.emotion);
|
||||
this.changeBondStrength(event.bondChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show telepathic message from Ana
|
||||
*/
|
||||
showTelepathicMessage(message, emotion = 'neutral') {
|
||||
console.log(`💭 Twin Bond Message: ${message}`);
|
||||
|
||||
// Update Ana's last message
|
||||
this.anaState.lastMessage = message;
|
||||
this.anaState.messageTime = Date.now();
|
||||
|
||||
// Show in dialogue system
|
||||
if (this.scene.dialogueSystem) {
|
||||
const anaData = {
|
||||
name: 'Ana (Twin Bond)',
|
||||
id: 'ana_telepathy'
|
||||
};
|
||||
|
||||
// Create temporary dialogue
|
||||
const telepathyDialogue = {
|
||||
root: 'message',
|
||||
nodes: {
|
||||
'message': {
|
||||
speaker: 'Ana (Twin Bond)',
|
||||
emotion: emotion,
|
||||
text: message,
|
||||
next: null // Auto-close
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.scene.dialogueSystem.registerDialogue('telepathy_' + Date.now(), telepathyDialogue);
|
||||
this.scene.dialogueSystem.startDialogue('telepathy_' + Date.now(), anaData);
|
||||
}
|
||||
|
||||
// Visual effect (bond pulse)
|
||||
this.scene.cameras.main.flash(500, 150, 100, 255, false);
|
||||
|
||||
// Bond strength change
|
||||
this.showBondPulse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change bond strength
|
||||
*/
|
||||
changeBondStrength(amount) {
|
||||
this.bondStrength = Phaser.Math.Clamp(
|
||||
this.bondStrength + amount,
|
||||
0,
|
||||
this.maxBondStrength
|
||||
);
|
||||
|
||||
console.log(`💞 Bond Strength: ${this.bondStrength.toFixed(1)}% (${amount > 0 ? '+' : ''}${amount})`);
|
||||
|
||||
// Notify player
|
||||
if (amount > 0) {
|
||||
this.scene.events.emit('bondStrengthened', { strength: this.bondStrength });
|
||||
} else {
|
||||
this.scene.events.emit('bondWeakened', { strength: this.bondStrength });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual bond pulse effect
|
||||
*/
|
||||
showBondPulse() {
|
||||
// TODO: Create particle effect at player position
|
||||
console.log('💫 Bond pulse visualization');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ability: Telepathy (send message to Ana)
|
||||
*/
|
||||
useTelepathy(message) {
|
||||
if (!this.abilities.telepathy.unlocked) {
|
||||
console.log('❌ Telepathy not unlocked');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.abilities.telepathy.cooldown > 0) {
|
||||
console.log('⏸️ Telepathy on cooldown');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`📡 Sending to Ana: ${message}`);
|
||||
|
||||
// Ana responds after delay
|
||||
this.scene.time.delayedCall(2000, () => {
|
||||
const responses = [
|
||||
"I heard you! Keep searching!",
|
||||
"Kai... I'm trying to stay strong...",
|
||||
"They don't know about our bond. Use that!",
|
||||
"I can feel you getting closer!"
|
||||
];
|
||||
|
||||
const response = Phaser.Utils.Array.GetRandom(responses);
|
||||
this.showTelepathicMessage(response, 'determined');
|
||||
});
|
||||
|
||||
// Set cooldown
|
||||
this.abilities.telepathy.cooldown = this.abilities.telepathy.maxCooldown;
|
||||
this.changeBondStrength(+2); // Strengthen bond
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ability: Sense Pulse (detect Ana's direction)
|
||||
*/
|
||||
useSensePulse() {
|
||||
if (!this.abilities.sensePulse.unlocked) {
|
||||
console.log('❌ Sense Pulse not unlocked');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.abilities.sensePulse.cooldown > 0) {
|
||||
console.log('⏸️ Sense Pulse on cooldown');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('📍 Sensing Ana\'s location...');
|
||||
|
||||
// Calculate general direction
|
||||
const playerX = this.scene.player?.x || 0;
|
||||
const playerY = this.scene.player?.y || 0;
|
||||
|
||||
const angle = Phaser.Math.Angle.Between(
|
||||
playerX, playerY,
|
||||
this.anaState.direction.x, this.anaState.direction.y
|
||||
);
|
||||
|
||||
const distance = this.anaState.distance;
|
||||
|
||||
// Show visual indicator
|
||||
this.showDirectionIndicator(angle, distance);
|
||||
|
||||
// Set cooldown
|
||||
this.abilities.sensePulse.cooldown = this.abilities.sensePulse.maxCooldown;
|
||||
this.changeBondStrength(+5);
|
||||
|
||||
return {
|
||||
angle: angle,
|
||||
distance: distance,
|
||||
distanceCategory: this.getDistanceCategory(distance)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get distance category (for vague communication)
|
||||
*/
|
||||
getDistanceCategory(distance) {
|
||||
if (distance < 500) return 'very_close';
|
||||
if (distance < 1500) return 'close';
|
||||
if (distance < 3000) return 'far';
|
||||
return 'very_far';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show direction indicator
|
||||
*/
|
||||
showDirectionIndicator(angle, distance) {
|
||||
const category = this.getDistanceCategory(distance);
|
||||
const messages = {
|
||||
'very_close': 'Ana is VERY CLOSE! ⬆️',
|
||||
'close': 'Ana is nearby 📍',
|
||||
'far': 'Ana is far away 🔭',
|
||||
'very_far': 'Ana is very far 🌌'
|
||||
};
|
||||
|
||||
console.log(`📍 ${messages[category]} (${Math.round(distance)}px)`);
|
||||
|
||||
// TODO: Show UI arrow pointing in direction
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Ana's danger level
|
||||
*/
|
||||
updateAnaDanger(deltaSeconds) {
|
||||
// Danger level increases over time (captors getting desperate)
|
||||
if (this.anaState.alive) {
|
||||
this.anaState.dangerLevel = Math.min(
|
||||
100,
|
||||
this.anaState.dangerLevel + 0.1 * deltaSeconds
|
||||
);
|
||||
|
||||
// Trigger danger events
|
||||
if (this.anaState.dangerLevel > 70 && Math.random() < 0.01) {
|
||||
const dangerEvent = this.bondEvents.find(e => e.id === 'danger_warning');
|
||||
if (dangerEvent) {
|
||||
this.showTelepathicMessage(dangerEvent.message, dangerEvent.emotion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Ana's position (for story progression)
|
||||
*/
|
||||
updateAnaLocation(x, y, distance) {
|
||||
this.anaState.direction.x = x;
|
||||
this.anaState.direction.y = y;
|
||||
this.anaState.distance = distance;
|
||||
|
||||
console.log(`📍 Ana's location updated: (${x}, ${y}), distance: ${distance}px`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Twin Bond UI
|
||||
*/
|
||||
createBondUI() {
|
||||
const width = this.scene.cameras.main.width;
|
||||
|
||||
// Bond meter (top-left)
|
||||
const x = 20;
|
||||
const y = 120;
|
||||
|
||||
// Background
|
||||
const bg = this.scene.add.rectangle(x, y, 200, 40, 0x2d1b00, 0.8);
|
||||
bg.setOrigin(0, 0);
|
||||
bg.setScrollFactor(0);
|
||||
bg.setDepth(100);
|
||||
|
||||
// Title
|
||||
const title = this.scene.add.text(x + 10, y + 5, '💞 Twin Bond', {
|
||||
fontSize: '14px',
|
||||
fontFamily: 'Georgia, serif',
|
||||
color: '#FFD700',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
title.setScrollFactor(0);
|
||||
title.setDepth(100);
|
||||
|
||||
// Bond bar
|
||||
const barBg = this.scene.add.rectangle(x + 10, y + 25, 180, 8, 0x000000, 0.8);
|
||||
barBg.setOrigin(0, 0);
|
||||
barBg.setScrollFactor(0);
|
||||
barBg.setDepth(100);
|
||||
|
||||
const barFill = this.scene.add.rectangle(
|
||||
x + 10, y + 25,
|
||||
180 * (this.bondStrength / 100),
|
||||
8,
|
||||
0xFF69B4,
|
||||
1
|
||||
);
|
||||
barFill.setOrigin(0, 0);
|
||||
barFill.setScrollFactor(0);
|
||||
barFill.setDepth(100);
|
||||
|
||||
this.bondUI = { bg, title, barBg, barFill };
|
||||
|
||||
// Update bar every frame
|
||||
this.scene.events.on('update', () => {
|
||||
if (this.bondUI && this.bondUI.barFill) {
|
||||
this.bondUI.barFill.width = 180 * (this.bondStrength / 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
getBondStrength() {
|
||||
return this.bondStrength;
|
||||
}
|
||||
|
||||
getAnaStatus() {
|
||||
return this.anaState;
|
||||
}
|
||||
|
||||
isAnaSafe() {
|
||||
return this.anaState.dangerLevel < 50;
|
||||
}
|
||||
}
|
||||
334
DEBUG_TOTAL_RECOVERY/TwinBondUISystem.js
Normal file
334
DEBUG_TOTAL_RECOVERY/TwinBondUISystem.js
Normal file
@@ -0,0 +1,334 @@
|
||||
/**
|
||||
* TwinBondUISystem.js
|
||||
* ====================
|
||||
* KRVAVA ŽETEV - Twin Bond Heartbeat UI
|
||||
*
|
||||
* Features:
|
||||
* - Visual heartbeat indicator on screen
|
||||
* - Speeds up when near Ana's clues
|
||||
* - Purple glow effect
|
||||
* - Distance-based intensity
|
||||
* - Twin bond strength indicator
|
||||
*
|
||||
* @author NovaFarma Team
|
||||
* @date 2025-12-25
|
||||
*/
|
||||
|
||||
export default class TwinBondUISystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// UI elements
|
||||
this.heartContainer = null;
|
||||
this.heartIcon = null;
|
||||
this.glowEffect = null;
|
||||
this.bondStrengthBar = null;
|
||||
|
||||
// State
|
||||
this.baseHeartRate = 60; // bpm
|
||||
this.currentHeartRate = 60;
|
||||
this.bondStrength = 0; // 0-100
|
||||
this.nearestClueDistance = Infinity;
|
||||
|
||||
// Animation
|
||||
this.heartbeatTween = null;
|
||||
this.lastBeat = 0;
|
||||
|
||||
console.log('💜 TwinBondUISystem initialized');
|
||||
|
||||
this.createUI();
|
||||
this.startHeartbeat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create UI elements
|
||||
*/
|
||||
createUI() {
|
||||
const ui = this.scene.scene.get('UIScene') || this.scene;
|
||||
|
||||
// Container (top-left corner)
|
||||
this.heartContainer = ui.add.container(50, 50);
|
||||
this.heartContainer.setDepth(1000); // Above everything
|
||||
this.heartContainer.setScrollFactor(0); // Fixed to camera
|
||||
|
||||
// Purple glow effect (behind heart)
|
||||
this.glowEffect = ui.add.circle(0, 0, 25, 0x9370DB, 0);
|
||||
this.heartContainer.add(this.glowEffect);
|
||||
|
||||
// Heart icon (emoji-style)
|
||||
this.heartIcon = ui.add.text(0, 0, '💜', {
|
||||
fontSize: '32px',
|
||||
fontFamily: 'Arial'
|
||||
});
|
||||
this.heartIcon.setOrigin(0.5);
|
||||
this.heartContainer.add(this.heartIcon);
|
||||
|
||||
// Bond strength bar (below heart)
|
||||
const barWidth = 50;
|
||||
const barHeight = 5;
|
||||
|
||||
// Background bar
|
||||
const barBg = ui.add.rectangle(0, 30, barWidth, barHeight, 0x333333);
|
||||
this.heartContainer.add(barBg);
|
||||
|
||||
// Strength bar
|
||||
this.bondStrengthBar = ui.add.rectangle(
|
||||
-barWidth / 2,
|
||||
30,
|
||||
0, // Start at 0 width
|
||||
barHeight,
|
||||
0x9370DB
|
||||
);
|
||||
this.bondStrengthBar.setOrigin(0, 0.5);
|
||||
this.heartContainer.add(this.bondStrengthBar);
|
||||
|
||||
// Bond strength text
|
||||
this.bondStrengthText = ui.add.text(0, 45, '0%', {
|
||||
fontSize: '10px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#9370DB'
|
||||
});
|
||||
this.bondStrengthText.setOrigin(0.5);
|
||||
this.heartContainer.add(this.bondStrengthText);
|
||||
|
||||
console.log('✅ Twin Bond UI created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start heartbeat animation
|
||||
*/
|
||||
startHeartbeat() {
|
||||
const beatInterval = () => {
|
||||
const bpm = this.currentHeartRate;
|
||||
const msPerBeat = 60000 / bpm; // Convert BPM to ms
|
||||
|
||||
// Heart pump animation
|
||||
if (this.heartIcon) {
|
||||
this.scene.tweens.add({
|
||||
targets: [this.heartIcon],
|
||||
scale: { from: 1, to: 1.3 },
|
||||
duration: 100,
|
||||
yoyo: true,
|
||||
ease: 'Quad.Out'
|
||||
});
|
||||
|
||||
// Glow pulse
|
||||
this.scene.tweens.add({
|
||||
targets: [this.glowEffect],
|
||||
alpha: { from: 0, to: 0.6 },
|
||||
scale: { from: 1, to: 1.5 },
|
||||
duration: 150,
|
||||
yoyo: true,
|
||||
ease: 'Quad.Out'
|
||||
});
|
||||
}
|
||||
|
||||
// Schedule next beat
|
||||
this.heartbeatTimer = setTimeout(() => beatInterval(), msPerBeat);
|
||||
};
|
||||
|
||||
// Start beating
|
||||
beatInterval();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update system (called every frame)
|
||||
*/
|
||||
update(time, delta) {
|
||||
if (!this.scene.player) return;
|
||||
|
||||
// Calculate distance to nearest Ana clue
|
||||
this.calculateNearestClue();
|
||||
|
||||
// Update heart rate based on distance
|
||||
this.updateHeartRate();
|
||||
|
||||
// Update bond strength display
|
||||
this.updateBondStrengthDisplay();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate distance to nearest Ana's clue
|
||||
*/
|
||||
calculateNearestClue() {
|
||||
if (!this.scene.anaClueSystem) {
|
||||
this.nearestClueDistance = Infinity;
|
||||
return;
|
||||
}
|
||||
|
||||
const playerX = this.scene.player.sprite.x;
|
||||
const playerY = this.scene.player.sprite.y;
|
||||
|
||||
let minDistance = Infinity;
|
||||
|
||||
// Check all clues
|
||||
this.scene.anaClueSystem.clueLocations.forEach((position, clueId) => {
|
||||
// Skip already discovered clues
|
||||
if (this.scene.anaClueSystem.discovered.has(clueId)) return;
|
||||
|
||||
const dx = position.x - playerX;
|
||||
const dy = position.y - playerY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
}
|
||||
});
|
||||
|
||||
this.nearestClueDistance = minDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update heart rate based on proximity to clues
|
||||
*/
|
||||
updateHeartRate() {
|
||||
const distance = this.nearestClueDistance;
|
||||
|
||||
// Distance thresholds (in pixels)
|
||||
const veryClose = 200; // 4-5 blocks
|
||||
const close = 480; // 10 blocks
|
||||
const medium = 960; // 20 blocks
|
||||
|
||||
let targetHeartRate = this.baseHeartRate;
|
||||
|
||||
if (distance < veryClose) {
|
||||
// VERY CLOSE - Rapid heartbeat!
|
||||
targetHeartRate = 120; // 2x normal
|
||||
this.bondStrength = 100;
|
||||
} else if (distance < close) {
|
||||
// CLOSE - Fast heartbeat
|
||||
targetHeartRate = 90;
|
||||
this.bondStrength = 75;
|
||||
} else if (distance < medium) {
|
||||
// MEDIUM - Slightly elevated
|
||||
targetHeartRate = 75;
|
||||
this.bondStrength = 50;
|
||||
} else {
|
||||
// FAR - Normal heartbeat
|
||||
targetHeartRate = 60;
|
||||
this.bondStrength = Math.max(0, this.bondStrength - 1); // Slowly decrease
|
||||
}
|
||||
|
||||
// Smooth transition
|
||||
this.currentHeartRate += (targetHeartRate - this.currentHeartRate) * 0.1;
|
||||
|
||||
// Clamp
|
||||
this.currentHeartRate = Phaser.Math.Clamp(this.currentHeartRate, 30, 150);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update bond strength visual display
|
||||
*/
|
||||
updateBondStrengthDisplay() {
|
||||
if (!this.bondStrengthBar) return;
|
||||
|
||||
// Update bar width
|
||||
const maxWidth = 50;
|
||||
const targetWidth = (this.bondStrength / 100) * maxWidth;
|
||||
this.bondStrengthBar.width = targetWidth;
|
||||
|
||||
// Update text
|
||||
if (this.bondStrengthText) {
|
||||
this.bondStrengthText.setText(`${Math.floor(this.bondStrength)}%`);
|
||||
}
|
||||
|
||||
// Color gradient based on strength
|
||||
if (this.bondStrength > 75) {
|
||||
this.bondStrengthBar.setFillStyle(0xFF69B4); // Hot pink - very strong
|
||||
} else if (this.bondStrength > 50) {
|
||||
this.bondStrengthBar.setFillStyle(0x9370DB); // Medium purple
|
||||
} else if (this.bondStrength > 25) {
|
||||
this.bondStrengthBar.setFillStyle(0x6A5ACD); // Slate blue
|
||||
} else {
|
||||
this.bondStrengthBar.setFillStyle(0x483D8B); // Dark slate blue
|
||||
}
|
||||
|
||||
// Pulsing glow when strong bond
|
||||
if (this.bondStrength > 75 && this.glowEffect) {
|
||||
this.glowEffect.setAlpha(0.3 + Math.sin(Date.now() / 200) * 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger special bond moment (e.g., flashback)
|
||||
*/
|
||||
triggerBondMoment(intensity = 'medium') {
|
||||
console.log(`💜 Twin Bond Moment! Intensity: ${intensity}`);
|
||||
|
||||
// Screen flash
|
||||
this.scene.cameras.main.flash(500, 147, 112, 219, false); // Purple flash
|
||||
|
||||
// Heart explosion effect
|
||||
if (this.heartIcon) {
|
||||
this.scene.tweens.add({
|
||||
targets: [this.heartIcon],
|
||||
scale: { from: 1, to: 2 },
|
||||
alpha: { from: 1, to: 0 },
|
||||
duration: 800,
|
||||
ease: 'Quad.Out',
|
||||
onComplete: () => {
|
||||
this.heartIcon.setScale(1);
|
||||
this.heartIcon.setAlpha(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Glow burst
|
||||
if (this.glowEffect) {
|
||||
this.scene.tweens.add({
|
||||
targets: [this.glowEffect],
|
||||
scale: { from: 1, to: 5 },
|
||||
alpha: { from: 0.8, to: 0 },
|
||||
duration: 1000,
|
||||
ease: 'Quad.Out',
|
||||
onComplete: () => {
|
||||
this.glowEffect.setScale(1);
|
||||
this.glowEffect.setAlpha(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Temporary heart rate spike
|
||||
this.currentHeartRate = 150;
|
||||
setTimeout(() => {
|
||||
this.currentHeartRate = this.baseHeartRate;
|
||||
}, 3000);
|
||||
|
||||
// Bond strength boost
|
||||
this.bondStrength = 100;
|
||||
|
||||
// Show notification
|
||||
this.scene.events.emit('show-notification', {
|
||||
title: 'Twin Bond Activated!',
|
||||
text: '💜 You feel Ana\'s presence intensify!',
|
||||
icon: '💜',
|
||||
color: '#9370DB'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current heart rate (for debugging)
|
||||
*/
|
||||
getCurrentHeartRate() {
|
||||
return Math.floor(this.currentHeartRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bond strength (for other systems)
|
||||
*/
|
||||
getBondStrength() {
|
||||
return this.bondStrength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearTimeout(this.heartbeatTimer);
|
||||
}
|
||||
if (this.heartContainer) {
|
||||
this.heartContainer.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
128
DEBUG_TOTAL_RECOVERY/VisualEffectsSystem.js
Normal file
128
DEBUG_TOTAL_RECOVERY/VisualEffectsSystem.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* VISUAL EFFECTS SYSTEM
|
||||
* Handles juice effects: screenshake, particles, lighting
|
||||
*/
|
||||
class VisualEffectsSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.camera = scene.cameras.main;
|
||||
this.isShaking = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Screenshake effect
|
||||
* @param {number} intensity - Shake strength (default 0.005)
|
||||
* @param {number} duration - Duration in ms (default 300)
|
||||
*/
|
||||
screenshake(intensity = 0.005, duration = 300) {
|
||||
if (this.isShaking) return;
|
||||
|
||||
this.isShaking = true;
|
||||
this.camera.shake(duration, intensity);
|
||||
|
||||
this.scene.time.delayedCall(duration, () => {
|
||||
this.isShaking = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit particles (sparks)
|
||||
*/
|
||||
createHitParticles(x, y, color = 0xFFFFFF) {
|
||||
const particles = this.scene.add.particles(x, y, 'particle_white', {
|
||||
speed: { min: 100, max: 200 },
|
||||
angle: { min: 0, max: 360 },
|
||||
scale: { start: 0.5, end: 0 },
|
||||
lifespan: 300,
|
||||
quantity: 8,
|
||||
tint: color
|
||||
});
|
||||
|
||||
this.scene.time.delayedCall(500, () => {
|
||||
particles.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Explosion particles
|
||||
*/
|
||||
createExplosion(x, y, color = 0xFF4444) {
|
||||
const particles = this.scene.add.particles(x, y, 'particle_white', {
|
||||
speed: { min: 200, max: 400 },
|
||||
angle: { min: 0, max: 360 },
|
||||
scale: { start: 1, end: 0 },
|
||||
lifespan: 600,
|
||||
quantity: 20,
|
||||
tint: color,
|
||||
gravityY: 300
|
||||
});
|
||||
|
||||
this.scene.time.delayedCall(800, () => {
|
||||
particles.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dust particles (for movement)
|
||||
*/
|
||||
createDustPuff(x, y) {
|
||||
const particles = this.scene.add.particles(x, y, 'particle_white', {
|
||||
speed: { min: 30, max: 60 },
|
||||
angle: { min: 0, max: 360 },
|
||||
scale: { start: 0.3, end: 0 },
|
||||
lifespan: 400,
|
||||
quantity: 5,
|
||||
tint: 0xCCBBAA,
|
||||
alpha: { start: 0.5, end: 0 }
|
||||
});
|
||||
|
||||
this.scene.time.delayedCall(500, () => {
|
||||
particles.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Flash effect (for damage, powerups)
|
||||
*/
|
||||
flash(color = 0xFFFFFF, duration = 100) {
|
||||
this.camera.flash(duration, ...this.hexToRGB(color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade effect
|
||||
*/
|
||||
fadeOut(duration = 1000, callback) {
|
||||
this.camera.fadeOut(duration, 0, 0, 0);
|
||||
if (callback) {
|
||||
this.scene.time.delayedCall(duration, callback);
|
||||
}
|
||||
}
|
||||
|
||||
fadeIn(duration = 1000) {
|
||||
this.camera.fadeIn(duration, 0, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: Hex to RGB
|
||||
*/
|
||||
hexToRGB(hex) {
|
||||
return [
|
||||
(hex >> 16) & 255,
|
||||
(hex >> 8) & 255,
|
||||
hex & 255
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create simple white pixel texture for particles
|
||||
*/
|
||||
static createParticleTexture(scene) {
|
||||
if (scene.textures.exists('particle_white')) return;
|
||||
|
||||
const graphics = scene.make.graphics({ x: 0, y: 0, add: false });
|
||||
graphics.fillStyle(0xFFFFFF, 1);
|
||||
graphics.fillCircle(4, 4, 4);
|
||||
graphics.generateTexture('particle_white', 8, 8);
|
||||
graphics.destroy();
|
||||
}
|
||||
}
|
||||
624
DEBUG_TOTAL_RECOVERY/VisualEnhancementSystem.js
Normal file
624
DEBUG_TOTAL_RECOVERY/VisualEnhancementSystem.js
Normal file
@@ -0,0 +1,624 @@
|
||||
/**
|
||||
* VISUAL ENHANCEMENT SYSTEM
|
||||
* Central system for managing visual effects, animations, and polish
|
||||
*/
|
||||
class VisualEnhancementSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.enabled = true;
|
||||
|
||||
// Sub-systems
|
||||
this.animatedTextures = new Map();
|
||||
this.weatherEffects = [];
|
||||
this.lightSources = [];
|
||||
this.shadows = [];
|
||||
this.particles = [];
|
||||
|
||||
// Settings
|
||||
this.settings = {
|
||||
animatedTextures: true,
|
||||
weatherEffects: true,
|
||||
dynamicLighting: true,
|
||||
shadows: true,
|
||||
fogOfWar: false,
|
||||
particleQuality: 'high', // low, medium, high, ultra
|
||||
animationQuality: 'high',
|
||||
screenShake: true,
|
||||
transitions: true
|
||||
};
|
||||
|
||||
// Animation timers
|
||||
this.waterAnimTime = 0;
|
||||
this.treeAnimTime = 0;
|
||||
this.fireAnimTime = 0;
|
||||
|
||||
this.loadSettings();
|
||||
this.init();
|
||||
|
||||
console.log('✅ Visual Enhancement System initialized');
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initAnimatedTextures();
|
||||
this.initWeatherEffects();
|
||||
this.initLightingSystem();
|
||||
this.initShadowSystem();
|
||||
this.initParticleSystem();
|
||||
}
|
||||
|
||||
// ========== ANIMATED TEXTURES ==========
|
||||
|
||||
initAnimatedTextures() {
|
||||
if (!this.settings.animatedTextures) return;
|
||||
|
||||
// Create water animation frames
|
||||
this.createWaterAnimation();
|
||||
|
||||
// Create fire animation
|
||||
this.createFireAnimation();
|
||||
|
||||
// Create tree leaf animation
|
||||
this.createTreeAnimation();
|
||||
|
||||
console.log('🎬 Animated textures initialized');
|
||||
}
|
||||
|
||||
createWaterAnimation() {
|
||||
// Water flow animation (4 frames)
|
||||
const frames = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const graphics = this.scene.add.graphics();
|
||||
graphics.fillStyle(0x4488ff, 1);
|
||||
graphics.fillRect(0, 0, 64, 64);
|
||||
|
||||
// Add wave pattern
|
||||
graphics.lineStyle(2, 0x6699ff, 0.5);
|
||||
for (let y = 0; y < 64; y += 8) {
|
||||
const offset = Math.sin((y + i * 16) * 0.1) * 4;
|
||||
graphics.lineBetween(0, y + offset, 64, y + offset);
|
||||
}
|
||||
|
||||
graphics.generateTexture('water_anim_' + i, 64, 64);
|
||||
graphics.destroy();
|
||||
frames.push('water_anim_' + i);
|
||||
}
|
||||
|
||||
this.animatedTextures.set('water', {
|
||||
frames,
|
||||
currentFrame: 0,
|
||||
speed: 200 // ms per frame
|
||||
});
|
||||
}
|
||||
|
||||
createFireAnimation() {
|
||||
// Fire flickering (3 frames)
|
||||
const frames = [];
|
||||
const colors = [0xff6600, 0xff8800, 0xffaa00];
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const graphics = this.scene.add.graphics();
|
||||
graphics.fillStyle(colors[i], 1);
|
||||
graphics.fillCircle(16, 16, 12 + i * 2);
|
||||
graphics.generateTexture('fire_anim_' + i, 32, 32);
|
||||
graphics.destroy();
|
||||
frames.push('fire_anim_' + i);
|
||||
}
|
||||
|
||||
this.animatedTextures.set('fire', {
|
||||
frames,
|
||||
currentFrame: 0,
|
||||
speed: 150
|
||||
});
|
||||
}
|
||||
|
||||
createTreeAnimation() {
|
||||
// Tree leaf rustling (subtle movement)
|
||||
console.log('🌳 Tree animation ready');
|
||||
}
|
||||
|
||||
updateAnimatedTextures(delta) {
|
||||
if (!this.settings.animatedTextures) return;
|
||||
|
||||
this.waterAnimTime += delta;
|
||||
this.fireAnimTime += delta;
|
||||
|
||||
// Update water
|
||||
const water = this.animatedTextures.get('water');
|
||||
if (water && this.waterAnimTime > water.speed) {
|
||||
water.currentFrame = (water.currentFrame + 1) % water.frames.length;
|
||||
this.waterAnimTime = 0;
|
||||
}
|
||||
|
||||
// Update fire
|
||||
const fire = this.animatedTextures.get('fire');
|
||||
if (fire && this.fireAnimTime > fire.speed) {
|
||||
fire.currentFrame = (fire.currentFrame + 1) % fire.frames.length;
|
||||
this.fireAnimTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== WEATHER EFFECTS ==========
|
||||
|
||||
initWeatherEffects() {
|
||||
if (!this.settings.weatherEffects) return;
|
||||
console.log('🌦️ Weather effects initialized');
|
||||
}
|
||||
|
||||
createSnowEffect() {
|
||||
const emitter = this.scene.add.particles(0, 0, 'particle_white', {
|
||||
x: { min: 0, max: this.scene.cameras.main.width },
|
||||
y: -10,
|
||||
speedY: { min: 50, max: 100 },
|
||||
speedX: { min: -20, max: 20 },
|
||||
scale: { min: 0.3, max: 0.8 },
|
||||
alpha: { min: 0.5, max: 1 },
|
||||
lifespan: 10000,
|
||||
frequency: 50,
|
||||
quantity: 2
|
||||
});
|
||||
emitter.setScrollFactor(0);
|
||||
this.weatherEffects.push(emitter);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
createRainEffect() {
|
||||
const emitter = this.scene.add.particles(0, 0, 'particle_white', {
|
||||
x: { min: 0, max: this.scene.cameras.main.width },
|
||||
y: -10,
|
||||
speedY: { min: 400, max: 600 },
|
||||
speedX: { min: -50, max: -30 },
|
||||
scaleX: 0.1,
|
||||
scaleY: 0.5,
|
||||
alpha: 0.6,
|
||||
lifespan: 2000,
|
||||
frequency: 10,
|
||||
quantity: 5,
|
||||
tint: 0x88ccff
|
||||
});
|
||||
emitter.setScrollFactor(0);
|
||||
this.weatherEffects.push(emitter);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
createLightningFlash() {
|
||||
const flash = this.scene.add.rectangle(
|
||||
this.scene.cameras.main.centerX,
|
||||
this.scene.cameras.main.centerY,
|
||||
this.scene.cameras.main.width,
|
||||
this.scene.cameras.main.height,
|
||||
0xffffff,
|
||||
0.8
|
||||
);
|
||||
flash.setScrollFactor(0);
|
||||
flash.setDepth(10000);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: flash,
|
||||
alpha: 0,
|
||||
duration: 200,
|
||||
onComplete: () => flash.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
createFogEffect() {
|
||||
// Atmospheric fog overlay - covers entire screen
|
||||
const fogOverlay = this.scene.add.graphics();
|
||||
fogOverlay.setScrollFactor(0);
|
||||
fogOverlay.setDepth(8000); // Above game, below UI
|
||||
fogOverlay.setAlpha(0.4); // Semi-transparent
|
||||
|
||||
// Create fog particles for movement effect
|
||||
const fogParticles = this.scene.add.particles(0, 0, 'particle_white', {
|
||||
x: { min: 0, max: this.scene.cameras.main.width },
|
||||
y: { min: 0, max: this.scene.cameras.main.height },
|
||||
speedX: { min: -10, max: 10 },
|
||||
speedY: { min: -5, max: 5 },
|
||||
scale: { min: 2, max: 4 },
|
||||
alpha: { min: 0.1, max: 0.3 },
|
||||
lifespan: 10000,
|
||||
frequency: 100,
|
||||
quantity: 3,
|
||||
tint: 0xcccccc,
|
||||
blendMode: 'SCREEN'
|
||||
});
|
||||
fogParticles.setScrollFactor(0);
|
||||
fogParticles.setDepth(8001);
|
||||
|
||||
// Animated fog overlay
|
||||
let fogTime = 0;
|
||||
const updateFog = () => {
|
||||
fogTime += 0.01;
|
||||
fogOverlay.clear();
|
||||
|
||||
const cam = this.scene.cameras.main;
|
||||
|
||||
// Draw multiple layers of fog for depth
|
||||
for (let layer = 0; layer < 3; layer++) {
|
||||
const offset = Math.sin(fogTime + layer) * 50;
|
||||
const alpha = 0.1 + layer * 0.05;
|
||||
|
||||
fogOverlay.fillStyle(0xffffff, alpha);
|
||||
|
||||
// Draw wavy fog shapes
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const x = (i * cam.width / 4) + offset;
|
||||
const y = (layer * 100) + Math.cos(fogTime + i) * 30;
|
||||
fogOverlay.fillCircle(x, y, 200 + layer * 50);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Update fog animation every frame
|
||||
this.scene.events.on('update', updateFog);
|
||||
|
||||
this.weatherEffects.push({
|
||||
overlay: fogOverlay,
|
||||
particles: fogParticles,
|
||||
update: updateFog,
|
||||
destroy: () => {
|
||||
fogOverlay.destroy();
|
||||
fogParticles.destroy();
|
||||
this.scene.events.off('update', updateFog);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🌫️ Atmospheric fog effect created');
|
||||
return { overlay: fogOverlay, particles: fogParticles };
|
||||
}
|
||||
|
||||
// ========== LIGHTING SYSTEM ==========
|
||||
|
||||
initLightingSystem() {
|
||||
if (!this.settings.dynamicLighting) return;
|
||||
|
||||
this.lightingLayer = this.scene.add.layer();
|
||||
this.lightingLayer.setDepth(5000);
|
||||
|
||||
console.log('💡 Lighting system initialized');
|
||||
}
|
||||
|
||||
addLight(x, y, radius = 100, color = 0xffaa00, intensity = 0.6) {
|
||||
if (!this.settings.dynamicLighting) return null;
|
||||
|
||||
// Create radial gradient light
|
||||
const light = this.scene.add.graphics();
|
||||
const gradient = light.createRadialGradient(
|
||||
radius, radius, 0,
|
||||
radius, radius, radius
|
||||
);
|
||||
|
||||
gradient.addColorStop(0, `rgba(${(color >> 16) & 0xff}, ${(color >> 8) & 0xff}, ${color & 0xff}, ${intensity})`);
|
||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
|
||||
light.fillStyle(color, intensity);
|
||||
light.fillCircle(radius, radius, radius);
|
||||
light.setPosition(x - radius, y - radius);
|
||||
light.setBlendMode(Phaser.BlendModes.ADD);
|
||||
light.setDepth(5001);
|
||||
|
||||
const lightObj = {
|
||||
graphics: light,
|
||||
x, y, radius, color, intensity,
|
||||
flickering: false
|
||||
};
|
||||
|
||||
this.lightSources.push(lightObj);
|
||||
return lightObj;
|
||||
}
|
||||
|
||||
addTorch(x, y) {
|
||||
const torch = this.addLight(x, y, 80, 0xff6600, 0.5);
|
||||
if (torch) {
|
||||
torch.flickering = true;
|
||||
}
|
||||
return torch;
|
||||
}
|
||||
|
||||
updateLighting(delta) {
|
||||
if (!this.settings.dynamicLighting) return;
|
||||
|
||||
// Update flickering lights
|
||||
for (const light of this.lightSources) {
|
||||
if (light.flickering) {
|
||||
const flicker = 0.4 + Math.random() * 0.2;
|
||||
light.graphics.setAlpha(flicker);
|
||||
}
|
||||
}
|
||||
|
||||
// Update ambient lighting based on time of day
|
||||
if (this.scene.weatherSystem) {
|
||||
const time = this.scene.weatherSystem.gameTime;
|
||||
const isNight = time < 6 || time > 18;
|
||||
|
||||
if (isNight) {
|
||||
// Darker at night
|
||||
this.scene.cameras.main.setAlpha(0.7);
|
||||
} else {
|
||||
this.scene.cameras.main.setAlpha(1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== SHADOW SYSTEM ==========
|
||||
|
||||
initShadowSystem() {
|
||||
if (!this.settings.shadows) return;
|
||||
console.log('🌑 Shadow system initialized');
|
||||
}
|
||||
|
||||
addShadow(entity, offsetX = 0, offsetY = 10, width = 40, height = 15) {
|
||||
if (!this.settings.shadows) return null;
|
||||
|
||||
const shadow = this.scene.add.ellipse(
|
||||
entity.x + offsetX,
|
||||
entity.y + offsetY,
|
||||
width,
|
||||
height,
|
||||
0x000000,
|
||||
0.3
|
||||
);
|
||||
shadow.setDepth(0);
|
||||
|
||||
const shadowObj = { entity, shadow, offsetX, offsetY };
|
||||
this.shadows.push(shadowObj);
|
||||
return shadow;
|
||||
}
|
||||
|
||||
updateShadows() {
|
||||
if (!this.settings.shadows) return;
|
||||
|
||||
// Get time of day for shadow opacity
|
||||
let opacity = 0.3;
|
||||
if (this.scene.weatherSystem) {
|
||||
const time = this.scene.weatherSystem.gameTime;
|
||||
// Darker shadows at noon, lighter at dawn/dusk
|
||||
opacity = 0.2 + Math.abs(Math.sin((time / 24) * Math.PI * 2)) * 0.3;
|
||||
}
|
||||
|
||||
// Update shadow positions
|
||||
for (const { entity, shadow, offsetX, offsetY } of this.shadows) {
|
||||
if (entity.sprite) {
|
||||
shadow.x = entity.sprite.x + offsetX;
|
||||
shadow.y = entity.sprite.y + offsetY;
|
||||
shadow.setAlpha(opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== PARTICLE SYSTEM ==========
|
||||
|
||||
initParticleSystem() {
|
||||
// Create particle textures
|
||||
this.createParticleTextures();
|
||||
console.log('✨ Particle system initialized');
|
||||
}
|
||||
|
||||
createParticleTextures() {
|
||||
// White particle
|
||||
const white = this.scene.add.graphics();
|
||||
white.fillStyle(0xffffff, 1);
|
||||
white.fillCircle(4, 4, 4);
|
||||
white.generateTexture('particle_white', 8, 8);
|
||||
white.destroy();
|
||||
|
||||
// Sparkle
|
||||
const sparkle = this.scene.add.graphics();
|
||||
sparkle.fillStyle(0xffff00, 1);
|
||||
sparkle.fillCircle(3, 3, 3);
|
||||
sparkle.generateTexture('particle_sparkle', 6, 6);
|
||||
sparkle.destroy();
|
||||
|
||||
// Heart
|
||||
const heart = this.scene.add.graphics();
|
||||
heart.fillStyle(0xff0066, 1);
|
||||
heart.fillCircle(3, 3, 3);
|
||||
heart.fillCircle(5, 3, 3);
|
||||
heart.fillTriangle(1, 4, 7, 4, 4, 8);
|
||||
heart.generateTexture('particle_heart', 8, 8);
|
||||
heart.destroy();
|
||||
}
|
||||
|
||||
createHeartParticles(x, y) {
|
||||
const emitter = this.scene.add.particles(x, y, 'particle_heart', {
|
||||
speed: { min: 20, max: 50 },
|
||||
angle: { min: -120, max: -60 },
|
||||
scale: { start: 1, end: 0 },
|
||||
alpha: { start: 1, end: 0 },
|
||||
lifespan: 1000,
|
||||
quantity: 5,
|
||||
blendMode: 'ADD'
|
||||
});
|
||||
|
||||
this.scene.time.delayedCall(1000, () => emitter.destroy());
|
||||
return emitter;
|
||||
}
|
||||
|
||||
createSparkleEffect(x, y) {
|
||||
const emitter = this.scene.add.particles(x, y, 'particle_sparkle', {
|
||||
speed: { min: 10, max: 30 },
|
||||
scale: { start: 1, end: 0 },
|
||||
alpha: { start: 1, end: 0 },
|
||||
lifespan: 800,
|
||||
quantity: 10,
|
||||
blendMode: 'ADD'
|
||||
});
|
||||
|
||||
this.scene.time.delayedCall(800, () => emitter.destroy());
|
||||
return emitter;
|
||||
}
|
||||
|
||||
createCheckmarkEffect(x, y) {
|
||||
const checkmark = this.scene.add.text(x, y, '✓', {
|
||||
fontSize: '32px',
|
||||
color: '#00ff00',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
checkmark.setOrigin(0.5);
|
||||
checkmark.setDepth(10000);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: checkmark,
|
||||
y: y - 50,
|
||||
alpha: 0,
|
||||
scale: 2,
|
||||
duration: 1000,
|
||||
ease: 'Power2',
|
||||
onComplete: () => checkmark.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
// ========== SCREEN EFFECTS ==========
|
||||
|
||||
screenShake(intensity = 10, duration = 300) {
|
||||
if (!this.settings.screenShake) return;
|
||||
this.scene.cameras.main.shake(duration, intensity / 1000);
|
||||
}
|
||||
|
||||
screenFlash(color = 0xffffff, duration = 200) {
|
||||
this.scene.cameras.main.flash(duration,
|
||||
(color >> 16) & 0xff,
|
||||
(color >> 8) & 0xff,
|
||||
color & 0xff
|
||||
);
|
||||
}
|
||||
|
||||
fadeOut(duration = 500, callback) {
|
||||
if (!this.settings.transitions) {
|
||||
if (callback) callback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.scene.cameras.main.fadeOut(duration, 0, 0, 0);
|
||||
if (callback) {
|
||||
this.scene.cameras.main.once('camerafadeoutcomplete', callback);
|
||||
}
|
||||
}
|
||||
|
||||
fadeIn(duration = 500) {
|
||||
if (!this.settings.transitions) return;
|
||||
this.scene.cameras.main.fadeIn(duration, 0, 0, 0);
|
||||
}
|
||||
|
||||
// ========== BUILDING EFFECTS ==========
|
||||
|
||||
createConstructionEffect(x, y) {
|
||||
// Dust particles during construction
|
||||
const emitter = this.scene.add.particles(x, y, 'particle_white', {
|
||||
speed: { min: 20, max: 40 },
|
||||
scale: { start: 0.5, end: 0 },
|
||||
alpha: { start: 0.5, end: 0 },
|
||||
lifespan: 1000,
|
||||
quantity: 3,
|
||||
frequency: 100,
|
||||
tint: 0x996633
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
createSmokeEffect(x, y) {
|
||||
// Chimney smoke
|
||||
const emitter = this.scene.add.particles(x, y, 'particle_white', {
|
||||
speedY: { min: -30, max: -50 },
|
||||
speedX: { min: -10, max: 10 },
|
||||
scale: { start: 0.3, end: 1 },
|
||||
alpha: { start: 0.5, end: 0 },
|
||||
lifespan: 2000,
|
||||
frequency: 500,
|
||||
quantity: 1,
|
||||
tint: 0x888888
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
// ========== FARM AUTOMATION VISUALS ==========
|
||||
|
||||
createPowerGridEffect(x1, y1, x2, y2) {
|
||||
// Electric arc between power sources
|
||||
const graphics = this.scene.add.graphics();
|
||||
graphics.lineStyle(2, 0x00ffff, 0.8);
|
||||
|
||||
// Draw lightning-like connection
|
||||
const steps = 5;
|
||||
const points = [];
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const x = x1 + (x2 - x1) * t + (Math.random() - 0.5) * 10;
|
||||
const y = y1 + (y2 - y1) * t + (Math.random() - 0.5) * 10;
|
||||
points.push({ x, y });
|
||||
}
|
||||
|
||||
graphics.beginPath();
|
||||
graphics.moveTo(points[0].x, points[0].y);
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
graphics.lineTo(points[i].x, points[i].y);
|
||||
}
|
||||
graphics.strokePath();
|
||||
|
||||
// Fade out
|
||||
this.scene.tweens.add({
|
||||
targets: graphics,
|
||||
alpha: 0,
|
||||
duration: 200,
|
||||
onComplete: () => graphics.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
createMutantGlow(entity, color = 0x00ff00) {
|
||||
// Radioactive glow for mutants
|
||||
const glow = this.scene.add.circle(
|
||||
entity.x,
|
||||
entity.y,
|
||||
30,
|
||||
color,
|
||||
0.3
|
||||
);
|
||||
glow.setBlendMode(Phaser.BlendModes.ADD);
|
||||
glow.setDepth(entity.depth - 1);
|
||||
|
||||
// Pulsing animation
|
||||
this.scene.tweens.add({
|
||||
targets: glow,
|
||||
scale: { from: 1, to: 1.2 },
|
||||
alpha: { from: 0.3, to: 0.1 },
|
||||
duration: 1000,
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
return glow;
|
||||
}
|
||||
|
||||
// ========== UPDATE ==========
|
||||
|
||||
update(delta) {
|
||||
this.updateAnimatedTextures(delta);
|
||||
this.updateLighting(delta);
|
||||
this.updateShadows();
|
||||
}
|
||||
|
||||
// ========== SETTINGS ==========
|
||||
|
||||
saveSettings() {
|
||||
localStorage.setItem('novafarma_visual_enhancements', JSON.stringify(this.settings));
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const saved = localStorage.getItem('novafarma_visual_enhancements');
|
||||
if (saved) {
|
||||
this.settings = { ...this.settings, ...JSON.parse(saved) };
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.lightingLayer) this.lightingLayer.destroy();
|
||||
for (const { shadow } of this.shadows) {
|
||||
shadow.destroy();
|
||||
}
|
||||
for (const effect of this.weatherEffects) {
|
||||
effect.destroy();
|
||||
}
|
||||
console.log('✨ Visual Enhancement System destroyed');
|
||||
}
|
||||
}
|
||||
746
DEBUG_TOTAL_RECOVERY/VisualSoundCueSystem.js
Normal file
746
DEBUG_TOTAL_RECOVERY/VisualSoundCueSystem.js
Normal file
@@ -0,0 +1,746 @@
|
||||
/**
|
||||
* VISUAL SOUND CUE SYSTEM
|
||||
* Provides visual indicators for sounds (accessibility for deaf/hard-of-hearing players)
|
||||
*/
|
||||
class VisualSoundCueSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.enabled = true;
|
||||
|
||||
// Visual elements
|
||||
this.heartbeatIndicator = null;
|
||||
this.damageIndicators = [];
|
||||
this.screenFlash = null;
|
||||
this.subtitleBackground = null;
|
||||
this.subtitleText = null;
|
||||
this.subtitleSpeaker = null;
|
||||
this.subtitleArrows = { left: null, right: null };
|
||||
this.heartbeatTween = null;
|
||||
this.fishingBobberIndicator = null;
|
||||
|
||||
// Speaker color mapping
|
||||
this.speakerColors = {
|
||||
'Player': '#00ff00',
|
||||
'NPC': '#ffff00',
|
||||
'Enemy': '#ff0000',
|
||||
'System': '#00ffff',
|
||||
'Narrator': '#ffffff'
|
||||
};
|
||||
|
||||
// Subtitle size presets
|
||||
this.subtitleSizes = {
|
||||
'small': { main: 16, speaker: 12, arrow: 24 },
|
||||
'medium': { main: 20, speaker: 16, arrow: 32 },
|
||||
'large': { main: 28, speaker: 20, arrow: 40 },
|
||||
'very-large': { main: 36, speaker: 24, arrow: 48 }
|
||||
};
|
||||
|
||||
// Settings
|
||||
this.settings = {
|
||||
heartbeatEnabled: true,
|
||||
damageIndicatorEnabled: true,
|
||||
screenFlashEnabled: true,
|
||||
subtitlesEnabled: true,
|
||||
directionalArrowsEnabled: true,
|
||||
speakerNamesEnabled: true,
|
||||
subtitleOpacity: 0.8,
|
||||
fishingBobberEnabled: true,
|
||||
subtitleSize: 'medium' // 'small', 'medium', 'large', 'very-large'
|
||||
};
|
||||
|
||||
this.loadSettings();
|
||||
this.init();
|
||||
|
||||
console.log('✅ Visual Sound Cue System initialized');
|
||||
}
|
||||
|
||||
init() {
|
||||
// Create heartbeat indicator (top-left corner)
|
||||
this.createHeartbeatIndicator();
|
||||
|
||||
// Create subtitle container (bottom center)
|
||||
this.createSubtitleContainer();
|
||||
|
||||
// Load settings from localStorage
|
||||
// this.loadSettings(); // Moved to constructor
|
||||
}
|
||||
|
||||
createHeartbeatIndicator() {
|
||||
const x = 200;
|
||||
const y = 30;
|
||||
|
||||
// Heart emoji as sprite
|
||||
this.heartbeatIndicator = this.scene.add.text(x, y, '❤️', {
|
||||
fontSize: '48px'
|
||||
});
|
||||
this.heartbeatIndicator.setOrigin(0.5);
|
||||
this.heartbeatIndicator.setDepth(10000);
|
||||
this.heartbeatIndicator.setScrollFactor(0);
|
||||
this.heartbeatIndicator.setVisible(false);
|
||||
}
|
||||
|
||||
createSubtitleContainer() {
|
||||
const width = this.scene.cameras.main.width;
|
||||
const height = this.scene.cameras.main.height;
|
||||
|
||||
// Background
|
||||
this.subtitleBackground = this.scene.add.rectangle(
|
||||
width / 2,
|
||||
height - 100,
|
||||
width - 100,
|
||||
100,
|
||||
0x000000,
|
||||
this.settings.subtitleOpacity
|
||||
);
|
||||
this.subtitleBackground.setOrigin(0.5);
|
||||
this.subtitleBackground.setDepth(9999);
|
||||
this.subtitleBackground.setScrollFactor(0);
|
||||
this.subtitleBackground.setVisible(false);
|
||||
|
||||
// Speaker name text
|
||||
this.subtitleSpeaker = this.scene.add.text(
|
||||
width / 2,
|
||||
height - 130,
|
||||
'',
|
||||
{
|
||||
fontSize: '16px',
|
||||
fontFamily: 'Arial',
|
||||
fontStyle: 'bold',
|
||||
color: '#ffffff',
|
||||
align: 'center'
|
||||
}
|
||||
);
|
||||
this.subtitleSpeaker.setOrigin(0.5);
|
||||
this.subtitleSpeaker.setDepth(10001);
|
||||
this.subtitleSpeaker.setScrollFactor(0);
|
||||
this.subtitleSpeaker.setVisible(false);
|
||||
|
||||
// Main subtitle text
|
||||
this.subtitleText = this.scene.add.text(
|
||||
width / 2,
|
||||
height - 100,
|
||||
'',
|
||||
{
|
||||
fontSize: '20px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#ffffff',
|
||||
align: 'center',
|
||||
wordWrap: { width: width - 160 }
|
||||
}
|
||||
);
|
||||
this.subtitleText.setOrigin(0.5);
|
||||
this.subtitleText.setDepth(10000);
|
||||
this.subtitleText.setScrollFactor(0);
|
||||
this.subtitleText.setVisible(false);
|
||||
|
||||
// Directional arrows
|
||||
this.subtitleArrows.left = this.scene.add.text(
|
||||
50,
|
||||
height - 100,
|
||||
'◄',
|
||||
{
|
||||
fontSize: '32px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#ffff00'
|
||||
}
|
||||
);
|
||||
this.subtitleArrows.left.setOrigin(0.5);
|
||||
this.subtitleArrows.left.setDepth(10001);
|
||||
this.subtitleArrows.left.setScrollFactor(0);
|
||||
this.subtitleArrows.left.setVisible(false);
|
||||
|
||||
this.subtitleArrows.right = this.scene.add.text(
|
||||
width - 50,
|
||||
height - 100,
|
||||
'►',
|
||||
{
|
||||
fontSize: '32px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#ffff00'
|
||||
}
|
||||
);
|
||||
this.subtitleArrows.right.setOrigin(0.5);
|
||||
this.subtitleArrows.right.setDepth(10001);
|
||||
this.subtitleArrows.right.setScrollFactor(0);
|
||||
this.subtitleArrows.right.setVisible(false);
|
||||
}
|
||||
|
||||
// ========== VISUAL HEARTBEAT (LOW HEALTH) ==========
|
||||
|
||||
updateHeartbeat(healthPercent) {
|
||||
if (!this.settings.heartbeatEnabled) return;
|
||||
|
||||
// Show heartbeat when health < 30%
|
||||
if (healthPercent < 30) {
|
||||
if (!this.heartbeatActive) {
|
||||
this.startHeartbeat(healthPercent);
|
||||
} else {
|
||||
this.updateHeartbeatSpeed(healthPercent);
|
||||
}
|
||||
} else {
|
||||
this.stopHeartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
startHeartbeat(healthPercent) {
|
||||
this.heartbeatActive = true;
|
||||
this.heartbeatIndicator.setVisible(true);
|
||||
|
||||
// Calculate speed based on health (lower health = faster beat)
|
||||
const speed = Phaser.Math.Clamp(1000 - (healthPercent * 20), 300, 1000);
|
||||
|
||||
this.heartbeatTween = this.scene.tweens.add({
|
||||
targets: this.heartbeatIndicator,
|
||||
scale: 1.3,
|
||||
alpha: 0.6,
|
||||
duration: speed / 2,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
|
||||
console.log('💓 Visual heartbeat started (health:', healthPercent + '%)');
|
||||
}
|
||||
|
||||
updateHeartbeatSpeed(healthPercent) {
|
||||
if (!this.heartbeatTween) return;
|
||||
|
||||
const speed = Phaser.Math.Clamp(1000 - (healthPercent * 20), 300, 1000);
|
||||
this.heartbeatTween.updateTo('duration', speed / 2, true);
|
||||
}
|
||||
|
||||
stopHeartbeat() {
|
||||
if (!this.heartbeatActive) return;
|
||||
|
||||
this.heartbeatActive = false;
|
||||
this.heartbeatIndicator.setVisible(false);
|
||||
|
||||
if (this.heartbeatTween) {
|
||||
this.heartbeatTween.stop();
|
||||
this.heartbeatTween = null;
|
||||
}
|
||||
|
||||
this.heartbeatIndicator.setScale(1);
|
||||
this.heartbeatIndicator.setAlpha(1);
|
||||
}
|
||||
|
||||
// ========== DAMAGE DIRECTION INDICATOR ==========
|
||||
|
||||
showDamageIndicator(direction, damage) {
|
||||
if (!this.settings.damageIndicatorEnabled) return;
|
||||
|
||||
const width = this.scene.cameras.main.width;
|
||||
const height = this.scene.cameras.main.height;
|
||||
|
||||
// Calculate position based on direction
|
||||
let x = width / 2;
|
||||
let y = height / 2;
|
||||
let arrow = '↓';
|
||||
let rotation = 0;
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
y = 100;
|
||||
arrow = '↑';
|
||||
rotation = 0;
|
||||
break;
|
||||
case 'down':
|
||||
y = height - 100;
|
||||
arrow = '↓';
|
||||
rotation = Math.PI;
|
||||
break;
|
||||
case 'left':
|
||||
x = 100;
|
||||
arrow = '←';
|
||||
rotation = -Math.PI / 2;
|
||||
break;
|
||||
case 'right':
|
||||
x = width - 100;
|
||||
arrow = '→';
|
||||
rotation = Math.PI / 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// Create damage indicator
|
||||
const indicator = this.scene.add.text(x, y, arrow, {
|
||||
fontSize: '64px',
|
||||
color: '#ff0000',
|
||||
fontStyle: 'bold'
|
||||
});
|
||||
indicator.setOrigin(0.5);
|
||||
indicator.setDepth(10001);
|
||||
indicator.setScrollFactor(0);
|
||||
indicator.setRotation(rotation);
|
||||
|
||||
// Animate and destroy
|
||||
this.scene.tweens.add({
|
||||
targets: indicator,
|
||||
alpha: 0,
|
||||
scale: 1.5,
|
||||
duration: 800,
|
||||
ease: 'Power2',
|
||||
onComplete: () => {
|
||||
indicator.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🎯 Damage indicator shown:', direction, damage);
|
||||
}
|
||||
|
||||
// ========== SCREEN FLASH NOTIFICATIONS ==========
|
||||
|
||||
showScreenFlash(type, message) {
|
||||
if (!this.settings.screenFlashEnabled) return;
|
||||
|
||||
const width = this.scene.cameras.main.width;
|
||||
const height = this.scene.cameras.main.height;
|
||||
|
||||
let color = 0xffffff;
|
||||
let icon = '⚠️';
|
||||
|
||||
switch (type) {
|
||||
case 'danger':
|
||||
color = 0xff0000;
|
||||
icon = '⚠️';
|
||||
break;
|
||||
case 'warning':
|
||||
color = 0xffaa00;
|
||||
icon = '⚡';
|
||||
break;
|
||||
case 'info':
|
||||
color = 0x00aaff;
|
||||
icon = 'ℹ️';
|
||||
break;
|
||||
case 'success':
|
||||
color = 0x00ff00;
|
||||
icon = '✓';
|
||||
break;
|
||||
}
|
||||
|
||||
// Flash overlay
|
||||
const flash = this.scene.add.rectangle(0, 0, width, height, color, 0.3);
|
||||
flash.setOrigin(0);
|
||||
flash.setDepth(9998);
|
||||
flash.setScrollFactor(0);
|
||||
|
||||
// Icon
|
||||
const iconText = this.scene.add.text(width / 2, height / 2, icon, {
|
||||
fontSize: '128px'
|
||||
});
|
||||
iconText.setOrigin(0.5);
|
||||
iconText.setDepth(9999);
|
||||
iconText.setScrollFactor(0);
|
||||
|
||||
// Message
|
||||
if (message) {
|
||||
this.showSubtitle(message, 2000);
|
||||
}
|
||||
|
||||
// Fade out
|
||||
this.scene.tweens.add({
|
||||
targets: [flash, iconText],
|
||||
alpha: 0,
|
||||
duration: 500,
|
||||
ease: 'Power2',
|
||||
onComplete: () => {
|
||||
flash.destroy();
|
||||
iconText.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('⚡ Screen flash shown:', type, message);
|
||||
}
|
||||
|
||||
// ========== SUBTITLES ==========
|
||||
|
||||
showSubtitle(text, duration = 3000, speaker = null, direction = null) {
|
||||
if (!this.settings.subtitlesEnabled) return;
|
||||
|
||||
// Set subtitle text
|
||||
this.subtitleText.setText(text);
|
||||
this.subtitleText.setVisible(true);
|
||||
this.subtitleBackground.setVisible(true);
|
||||
|
||||
// Show speaker name with color if enabled
|
||||
if (speaker && this.settings.speakerNamesEnabled) {
|
||||
const color = this.speakerColors[speaker] || '#ffffff';
|
||||
this.subtitleSpeaker.setText(speaker);
|
||||
this.subtitleSpeaker.setColor(color);
|
||||
this.subtitleSpeaker.setVisible(true);
|
||||
} else {
|
||||
this.subtitleSpeaker.setVisible(false);
|
||||
}
|
||||
|
||||
// Show directional arrows if enabled and direction provided
|
||||
if (direction && this.settings.directionalArrowsEnabled) {
|
||||
this.showDirectionalArrows(direction);
|
||||
} else {
|
||||
this.hideDirectionalArrows();
|
||||
}
|
||||
|
||||
// Auto-hide after duration
|
||||
this.scene.time.delayedCall(duration, () => {
|
||||
this.hideSubtitle();
|
||||
});
|
||||
|
||||
console.log('💬 Subtitle shown:', speaker ? `[${speaker}] ${text}` : text);
|
||||
}
|
||||
|
||||
showDirectionalArrows(direction) {
|
||||
this.hideDirectionalArrows();
|
||||
|
||||
if (direction === 'left' || direction === 'west') {
|
||||
this.subtitleArrows.left.setVisible(true);
|
||||
// Pulse animation
|
||||
this.scene.tweens.add({
|
||||
targets: this.subtitleArrows.left,
|
||||
alpha: { from: 1, to: 0.3 },
|
||||
duration: 500,
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
} else if (direction === 'right' || direction === 'east') {
|
||||
this.subtitleArrows.right.setVisible(true);
|
||||
// Pulse animation
|
||||
this.scene.tweens.add({
|
||||
targets: this.subtitleArrows.right,
|
||||
alpha: { from: 1, to: 0.3 },
|
||||
duration: 500,
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
} else if (direction === 'both') {
|
||||
this.subtitleArrows.left.setVisible(true);
|
||||
this.subtitleArrows.right.setVisible(true);
|
||||
// Pulse animation for both
|
||||
this.scene.tweens.add({
|
||||
targets: [this.subtitleArrows.left, this.subtitleArrows.right],
|
||||
alpha: { from: 1, to: 0.3 },
|
||||
duration: 500,
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hideDirectionalArrows() {
|
||||
this.scene.tweens.killTweensOf(this.subtitleArrows.left);
|
||||
this.scene.tweens.killTweensOf(this.subtitleArrows.right);
|
||||
this.subtitleArrows.left.setVisible(false);
|
||||
this.subtitleArrows.right.setVisible(false);
|
||||
this.subtitleArrows.left.setAlpha(1);
|
||||
this.subtitleArrows.right.setAlpha(1);
|
||||
}
|
||||
|
||||
hideSubtitle() {
|
||||
this.subtitleText.setVisible(false);
|
||||
this.subtitleBackground.setVisible(false);
|
||||
this.subtitleSpeaker.setVisible(false);
|
||||
this.hideDirectionalArrows();
|
||||
}
|
||||
|
||||
// ========== SOUND EVENT HANDLERS ==========
|
||||
|
||||
onSoundPlayed(soundType, data = {}) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
switch (soundType) {
|
||||
case 'damage':
|
||||
this.showDamageIndicator(data.direction || 'down', data.amount || 10);
|
||||
this.showSubtitle('[DAMAGE TAKEN]', 1500, 'System', data.direction);
|
||||
break;
|
||||
|
||||
case 'pickup':
|
||||
this.showSubtitle(`[PICKED UP: ${data.item || 'Item'}]`, 1500, 'System');
|
||||
break;
|
||||
|
||||
case 'harvest':
|
||||
this.showSubtitle('[CROP HARVESTED]', 1500, 'System');
|
||||
break;
|
||||
|
||||
case 'build':
|
||||
this.showSubtitle('[BUILDING PLACED]', 1500, 'System');
|
||||
break;
|
||||
|
||||
case 'dig':
|
||||
this.showSubtitle('[DIGGING SOUND]', 1000, 'System');
|
||||
break;
|
||||
|
||||
case 'plant':
|
||||
this.showSubtitle('[PLANTING SOUND]', 1000, 'System');
|
||||
break;
|
||||
|
||||
case 'footsteps':
|
||||
this.showSubtitle('[FOOTSTEPS]', 500, null, data.direction);
|
||||
break;
|
||||
|
||||
case 'door':
|
||||
this.showSubtitle('[DOOR OPENS]', 1000, 'System');
|
||||
break;
|
||||
|
||||
case 'chest':
|
||||
this.showSubtitle('[CHEST OPENS]', 1000, 'System');
|
||||
break;
|
||||
|
||||
case 'water':
|
||||
this.showSubtitle('[WATER SPLASH]', 1000, 'System');
|
||||
break;
|
||||
|
||||
case 'fire':
|
||||
this.showSubtitle('[FIRE CRACKLING]', 2000, 'System');
|
||||
break;
|
||||
|
||||
case 'explosion':
|
||||
this.showSubtitle('[EXPLOSION!]', 1500, 'System');
|
||||
this.showScreenFlash('danger', '[EXPLOSION!]');
|
||||
break;
|
||||
|
||||
case 'npc_talk':
|
||||
this.showSubtitle(data.text || '[NPC TALKING]', 3000, data.speaker || 'NPC', data.direction);
|
||||
break;
|
||||
|
||||
case 'enemy_growl':
|
||||
this.showSubtitle('[ENEMY GROWL]', 1500, 'Enemy', data.direction);
|
||||
break;
|
||||
|
||||
case 'fishing_cast':
|
||||
this.showSubtitle('[FISHING LINE CAST]', 1000, 'System');
|
||||
break;
|
||||
|
||||
case 'fishing_bite':
|
||||
this.showSubtitle('[FISH BITING!]', 1500, 'System');
|
||||
this.showFishingBobberCue();
|
||||
break;
|
||||
|
||||
case 'danger':
|
||||
this.showScreenFlash('danger', '[DANGER!]');
|
||||
this.showSubtitle('[DANGER NEARBY]', 2000, 'System');
|
||||
break;
|
||||
|
||||
case 'night':
|
||||
this.showScreenFlash('warning', '[NIGHT FALLING]');
|
||||
this.showSubtitle('[NIGHT IS FALLING]', 2000, 'System');
|
||||
break;
|
||||
|
||||
case 'achievement':
|
||||
this.showScreenFlash('success', data.message || '[ACHIEVEMENT UNLOCKED]');
|
||||
this.showSubtitle(data.message || '[ACHIEVEMENT UNLOCKED]', 3000, 'System');
|
||||
break;
|
||||
|
||||
case 'ui_click':
|
||||
this.showSubtitle('[CLICK]', 300, null);
|
||||
break;
|
||||
|
||||
case 'ui_hover':
|
||||
this.showSubtitle('[HOVER]', 200, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show fishing bobber visual cue
|
||||
*/
|
||||
showFishingBobberCue() {
|
||||
if (!this.settings.fishingBobberEnabled) return;
|
||||
|
||||
const width = this.scene.cameras.main.width;
|
||||
const height = this.scene.cameras.main.height;
|
||||
|
||||
// Create bobber indicator if it doesn't exist
|
||||
if (!this.fishingBobberIndicator) {
|
||||
this.fishingBobberIndicator = this.scene.add.container(width / 2, height / 2);
|
||||
this.fishingBobberIndicator.setDepth(10002);
|
||||
this.fishingBobberIndicator.setScrollFactor(0);
|
||||
|
||||
// Circle background
|
||||
const circle = this.scene.add.circle(0, 0, 60, 0xff6600, 0.8);
|
||||
this.fishingBobberIndicator.add(circle);
|
||||
|
||||
// Exclamation mark
|
||||
const exclamation = this.scene.add.text(0, 0, '!', {
|
||||
fontSize: '48px',
|
||||
fontFamily: 'Arial',
|
||||
fontStyle: 'bold',
|
||||
color: '#ffffff'
|
||||
});
|
||||
exclamation.setOrigin(0.5);
|
||||
this.fishingBobberIndicator.add(exclamation);
|
||||
|
||||
// Text below
|
||||
const text = this.scene.add.text(0, 80, 'FISH BITING!\nPress E', {
|
||||
fontSize: '20px',
|
||||
fontFamily: 'Arial',
|
||||
fontStyle: 'bold',
|
||||
color: '#ffffff',
|
||||
align: 'center'
|
||||
});
|
||||
text.setOrigin(0.5);
|
||||
this.fishingBobberIndicator.add(text);
|
||||
}
|
||||
|
||||
// Show and animate
|
||||
this.fishingBobberIndicator.setVisible(true);
|
||||
this.fishingBobberIndicator.setAlpha(0);
|
||||
|
||||
// Fade in and pulse
|
||||
this.scene.tweens.add({
|
||||
targets: this.fishingBobberIndicator,
|
||||
alpha: 1,
|
||||
duration: 200,
|
||||
onComplete: () => {
|
||||
// Pulse animation
|
||||
this.scene.tweens.add({
|
||||
targets: this.fishingBobberIndicator,
|
||||
scale: { from: 1, to: 1.2 },
|
||||
duration: 300,
|
||||
yoyo: true,
|
||||
repeat: 5,
|
||||
onComplete: () => {
|
||||
// Fade out
|
||||
this.scene.tweens.add({
|
||||
targets: this.fishingBobberIndicator,
|
||||
alpha: 0,
|
||||
duration: 300,
|
||||
onComplete: () => {
|
||||
this.fishingBobberIndicator.setVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🎣 Fishing bobber cue shown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set subtitle background opacity
|
||||
*/
|
||||
setSubtitleOpacity(opacity) {
|
||||
this.settings.subtitleOpacity = Phaser.Math.Clamp(opacity, 0, 1);
|
||||
if (this.subtitleBackground) {
|
||||
this.subtitleBackground.setAlpha(this.settings.subtitleOpacity);
|
||||
}
|
||||
this.saveSettings();
|
||||
console.log('📊 Subtitle opacity set to:', this.settings.subtitleOpacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom speaker color
|
||||
*/
|
||||
addSpeakerColor(speaker, color) {
|
||||
this.speakerColors[speaker] = color;
|
||||
console.log(`🎨 Added speaker color: ${speaker} = ${color}`);
|
||||
}
|
||||
|
||||
// ========== SETTINGS ==========
|
||||
|
||||
toggleHeartbeat(enabled) {
|
||||
this.settings.heartbeatEnabled = enabled;
|
||||
if (!enabled) this.stopHeartbeat();
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
toggleDamageIndicator(enabled) {
|
||||
this.settings.damageIndicatorEnabled = enabled;
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
toggleScreenFlash(enabled) {
|
||||
this.settings.screenFlashEnabled = enabled;
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
toggleSubtitles(enabled) {
|
||||
this.settings.subtitlesEnabled = enabled;
|
||||
if (!enabled) this.hideSubtitle();
|
||||
this.saveSettings();
|
||||
console.log('💬 Subtitles:', enabled ? 'ENABLED' : 'DISABLED');
|
||||
}
|
||||
|
||||
toggleDirectionalArrows(enabled) {
|
||||
this.settings.directionalArrowsEnabled = enabled;
|
||||
if (!enabled) {
|
||||
this.hideDirectionalArrows();
|
||||
}
|
||||
this.saveSettings();
|
||||
console.log('➡️ Directional Arrows:', enabled ? 'ENABLED' : 'DISABLED');
|
||||
}
|
||||
|
||||
toggleSpeakerNames(enabled) {
|
||||
this.settings.speakerNamesEnabled = enabled;
|
||||
this.saveSettings();
|
||||
console.log('👤 Speaker Names:', enabled ? 'ENABLED' : 'DISABLED');
|
||||
}
|
||||
|
||||
toggleFishingBobber(enabled) {
|
||||
this.settings.fishingBobberEnabled = enabled;
|
||||
this.saveSettings();
|
||||
console.log('🎣 Fishing Bobber Cue:', enabled ? 'ENABLED' : 'DISABLED');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set subtitle text size
|
||||
* @param {string} size - 'small', 'medium', 'large', 'very-large'
|
||||
*/
|
||||
setSubtitleSize(size) {
|
||||
if (!this.subtitleSizes[size]) {
|
||||
console.error(`Invalid subtitle size: ${size}. Valid options: small, medium, large, very-large`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.settings.subtitleSize = size;
|
||||
const sizes = this.subtitleSizes[size];
|
||||
|
||||
// Update text sizes
|
||||
if (this.subtitleText) {
|
||||
this.subtitleText.setFontSize(sizes.main);
|
||||
}
|
||||
if (this.subtitleSpeaker) {
|
||||
this.subtitleSpeaker.setFontSize(sizes.speaker);
|
||||
}
|
||||
if (this.subtitleArrows.left) {
|
||||
this.subtitleArrows.left.setFontSize(sizes.arrow);
|
||||
}
|
||||
if (this.subtitleArrows.right) {
|
||||
this.subtitleArrows.right.setFontSize(sizes.arrow);
|
||||
}
|
||||
|
||||
// Adjust background height based on text size
|
||||
if (this.subtitleBackground) {
|
||||
const bgHeight = sizes.main * 4; // 4x font size for padding
|
||||
this.subtitleBackground.setSize(this.subtitleBackground.width, bgHeight);
|
||||
}
|
||||
|
||||
this.saveSettings();
|
||||
console.log(`📏 Subtitle size set to: ${size.toUpperCase()} (${sizes.main}px)`);
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
localStorage.setItem('novafarma_visual_sound_cues', JSON.stringify(this.settings));
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const saved = localStorage.getItem('novafarma_visual_sound_cues');
|
||||
if (saved) {
|
||||
this.settings = { ...this.settings, ...JSON.parse(saved) };
|
||||
}
|
||||
}
|
||||
|
||||
// ========== UPDATE ==========
|
||||
|
||||
update() {
|
||||
// Update heartbeat based on player health
|
||||
if (this.scene.player && this.scene.player.health !== undefined) {
|
||||
const healthPercent = (this.scene.player.health / this.scene.player.maxHealth) * 100;
|
||||
this.updateHeartbeat(healthPercent);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.heartbeatTween) this.heartbeatTween.stop();
|
||||
if (this.heartbeatSprite) this.heartbeatSprite.destroy();
|
||||
if (this.subtitleText) this.subtitleText.destroy();
|
||||
if (this.subtitleBackground) this.subtitleBackground.destroy();
|
||||
}
|
||||
}
|
||||
361
DEBUG_TOTAL_RECOVERY/ZombieCommunicationSystem.js
Normal file
361
DEBUG_TOTAL_RECOVERY/ZombieCommunicationSystem.js
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* ZombieCommunicationSystem.js
|
||||
* =============================
|
||||
* KRVAVA ŽETEV - Zombie Communication System (Hybrid Skill)
|
||||
*
|
||||
* Features:
|
||||
* - Level-based zombie understanding
|
||||
* - Level 1: Groaning only ("Hnggg...")
|
||||
* - Level 5: Keywords in subtitles
|
||||
* - Level 10: Full sentences (warnings, memories)
|
||||
* - Subtitle UI
|
||||
* - Translation system
|
||||
*
|
||||
* @author NovaFarma Team
|
||||
* @date 2025-12-23
|
||||
*/
|
||||
|
||||
export default class ZombieCommunicationSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Player's communication level
|
||||
this.communicationLevel = 0;
|
||||
this.maxLevel = 10;
|
||||
|
||||
// Subtitle UI
|
||||
this.subtitleText = null;
|
||||
this.subtitleContainer = null;
|
||||
this.currentSubtitle = null;
|
||||
|
||||
// Zombie speech library
|
||||
this.zombiePhrases = new Map();
|
||||
|
||||
console.log('🧠 ZombieCommunicationSystem initialized');
|
||||
|
||||
// Create subtitle UI
|
||||
this.createSubtitleUI();
|
||||
|
||||
// Load zombie phrases
|
||||
this.loadZombiePhrases();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create subtitle UI
|
||||
*/
|
||||
createSubtitleUI() {
|
||||
const width = this.scene.cameras.main.width;
|
||||
const height = this.scene.cameras.main.height;
|
||||
|
||||
// Container at bottom of screen
|
||||
this.subtitleContainer = this.scene.add.container(width / 2, height - 100);
|
||||
this.subtitleContainer.setScrollFactor(0);
|
||||
this.subtitleContainer.setDepth(10000);
|
||||
this.subtitleContainer.setAlpha(0);
|
||||
|
||||
// Background
|
||||
const bg = this.scene.add.rectangle(0, 0, 800, 100, 0x000000, 0.8);
|
||||
this.subtitleContainer.add(bg);
|
||||
|
||||
// Subtitle text
|
||||
this.subtitleText = this.scene.add.text(0, 0, '', {
|
||||
fontSize: '24px',
|
||||
fontFamily: 'Arial',
|
||||
color: '#00FF00', // Green zombie text
|
||||
align: 'center',
|
||||
wordWrap: { width: 750 }
|
||||
});
|
||||
this.subtitleText.setOrigin(0.5);
|
||||
this.subtitleContainer.add(this.subtitleText);
|
||||
|
||||
console.log('✅ Subtitle UI created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load zombie phrase library
|
||||
*/
|
||||
loadZombiePhrases() {
|
||||
// Level 1: Pure groaning
|
||||
const level1Phrases = [
|
||||
{ zombie: 'Hnggg...', translation: null },
|
||||
{ zombie: 'Grrraaa...', translation: null },
|
||||
{ zombie: 'Uuuhhh...', translation: null },
|
||||
{ zombie: 'Aaarrgh...', translation: null }
|
||||
];
|
||||
|
||||
// Level 5: Keywords visible
|
||||
const level5Phrases = [
|
||||
{ zombie: 'Hnggg... HUNGER... grrr...', translation: 'I am hungry...' },
|
||||
{ zombie: 'Grrr... DANGER... hnggg...', translation: 'Danger nearby!' },
|
||||
{ zombie: 'Uhhh... MASTER... grrr...', translation: 'Looking for master...' },
|
||||
{ zombie: 'Aaah... PAIN... hnggg...', translation: 'I am in pain...' },
|
||||
{ zombie: 'Grrr... HELP... uhhh...', translation: 'Help me...' },
|
||||
{ zombie: 'Hnggg... FRIEND... grrr...', translation: 'You are my friend...' }
|
||||
];
|
||||
|
||||
// Level 10: Full sentences
|
||||
const level10Phrases = [
|
||||
{ zombie: 'I remember... my family...', translation: 'I remember my family before I turned...' },
|
||||
{ zombie: 'The darkness... it hurts...', translation: 'The curse is painful...' },
|
||||
{ zombie: 'Thank you... for saving me...', translation: 'Thank you for taming me instead of killing me.' },
|
||||
{ zombie: 'Enemies... coming from east...', translation: 'I sense enemies approaching from the east!' },
|
||||
{ zombie: 'My name was... John...', translation: 'I remember my name was John...' },
|
||||
{ zombie: 'Ana... she calls us...', translation: 'Ana\'s Twin Bond resonates with us zombies...' },
|
||||
{ zombie: 'The Black Serpent... did this...', translation: 'The Black Serpent Initiative caused this outbreak.' },
|
||||
{ zombie: 'I was a farmer... before...', translation: 'I was a farmer before the infection...' },
|
||||
{ zombie: 'Danger! Big zombie nearby!', translation: 'WARNING: Boss zombie detected!' },
|
||||
{ zombie: 'I protect you... master...', translation: 'I will protect you with my unlife, master.' }
|
||||
];
|
||||
|
||||
// Special contextual phrases
|
||||
const contextualPhrases = [
|
||||
{ context: 'low_health', zombie: 'Hnggg... weak... dying...', translation: 'I am badly hurt!' },
|
||||
{ context: 'enemy_near', zombie: 'Grrr! Intruders!', translation: 'Enemies detected!' },
|
||||
{ context: 'happy', zombie: 'Grraaa... good... happy...', translation: 'I am happy serving you!' },
|
||||
{ context: 'task_complete', zombie: 'Uhhh... done... master...', translation: 'Task completed, master!' },
|
||||
{ context: 'hungry', zombie: 'Need... food... hnggg...', translation: 'I need to eat soon...' },
|
||||
{ context: 'scared', zombie: 'Aaaah! Fear! Run!', translation: 'Something terrifying is here!' }
|
||||
];
|
||||
|
||||
this.zombiePhrases.set('level1', level1Phrases);
|
||||
this.zombiePhrases.set('level5', level5Phrases);
|
||||
this.zombiePhrases.set('level10', level10Phrases);
|
||||
this.zombiePhrases.set('contextual', contextualPhrases);
|
||||
|
||||
console.log(`✅ Loaded ${level1Phrases.length + level5Phrases.length + level10Phrases.length + contextualPhrases.length} zombie phrases`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set communication level
|
||||
*/
|
||||
setCommunicationLevel(level) {
|
||||
this.communicationLevel = Math.min(this.maxLevel, Math.max(0, level));
|
||||
|
||||
console.log(`🧠 Communication level: ${this.communicationLevel}/10`);
|
||||
|
||||
this.showNotification({
|
||||
title: 'Zombie Understanding Improved!',
|
||||
text: `🧠 Level ${this.communicationLevel}: ${this.getLevelDescription()}`,
|
||||
icon: '🧟'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get level description
|
||||
*/
|
||||
getLevelDescription() {
|
||||
if (this.communicationLevel >= 10) {
|
||||
return 'Full sentences! You understand zombies completely!';
|
||||
} else if (this.communicationLevel >= 5) {
|
||||
return 'Keywords visible! You understand basic meanings!';
|
||||
} else {
|
||||
return 'Only groaning... You need more practice!';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zombie speaks
|
||||
*/
|
||||
zombieSpeak(zombieId, context = null) {
|
||||
let phrase;
|
||||
|
||||
// Get appropriate phrase based on level
|
||||
if (context) {
|
||||
phrase = this.getContextualPhrase(context);
|
||||
} else if (this.communicationLevel >= 10) {
|
||||
phrase = this.getRandomPhrase('level10');
|
||||
} else if (this.communicationLevel >= 5) {
|
||||
phrase = this.getRandomPhrase('level5');
|
||||
} else {
|
||||
phrase = this.getRandomPhrase('level1');
|
||||
}
|
||||
|
||||
if (!phrase) return;
|
||||
|
||||
// Show subtitle
|
||||
this.showSubtitle(phrase, zombieId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get random phrase from level
|
||||
*/
|
||||
getRandomPhrase(level) {
|
||||
const phrases = this.zombiePhrases.get(level);
|
||||
if (!phrases || phrases.length === 0) return null;
|
||||
|
||||
return Phaser.Utils.Array.GetRandom(phrases);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get contextual phrase
|
||||
*/
|
||||
getContextualPhrase(context) {
|
||||
const phrases = this.zombiePhrases.get('contextual');
|
||||
const found = phrases.filter(p => p.context === context);
|
||||
|
||||
if (found.length === 0) return this.getRandomPhrase('level1');
|
||||
|
||||
return Phaser.Utils.Array.GetRandom(found);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show subtitle
|
||||
*/
|
||||
showSubtitle(phrase, zombieId = 'Zombie') {
|
||||
let displayText = phrase.zombie;
|
||||
|
||||
// Add translation if level is high enough
|
||||
if (this.communicationLevel >= 5 && phrase.translation) {
|
||||
displayText += `\n[${phrase.translation}]`;
|
||||
}
|
||||
|
||||
// Show speaker name
|
||||
displayText = `${zombieId}: ${displayText}`;
|
||||
|
||||
this.subtitleText.setText(displayText);
|
||||
|
||||
// Fade in
|
||||
this.scene.tweens.add({
|
||||
targets: this.subtitleContainer,
|
||||
alpha: 1,
|
||||
duration: 300
|
||||
});
|
||||
|
||||
// Auto-hide after 3 seconds
|
||||
if (this.currentSubtitle) {
|
||||
clearTimeout(this.currentSubtitle);
|
||||
}
|
||||
|
||||
this.currentSubtitle = setTimeout(() => {
|
||||
this.hideSubtitle();
|
||||
}, 3000);
|
||||
|
||||
console.log(`💬 ${displayText}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide subtitle
|
||||
*/
|
||||
hideSubtitle() {
|
||||
this.scene.tweens.add({
|
||||
targets: this.subtitleContainer,
|
||||
alpha: 0,
|
||||
duration: 300
|
||||
});
|
||||
|
||||
this.currentSubtitle = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zombie conversation (interactive)
|
||||
*/
|
||||
startConversation(zombieId) {
|
||||
if (this.communicationLevel < 5) {
|
||||
this.showSubtitle({
|
||||
zombie: 'Hnggg... grrr...',
|
||||
translation: null
|
||||
}, zombieId);
|
||||
|
||||
this.showNotification({
|
||||
title: 'Cannot Understand',
|
||||
text: 'Your zombie communication skill is too low!',
|
||||
icon: '🧠'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`💬 Conversation with ${zombieId}`);
|
||||
|
||||
// Show conversation phrases
|
||||
const phrases = [
|
||||
'What is your name?',
|
||||
'What do you remember?',
|
||||
'Are you loyal?',
|
||||
'Do you feel pain?',
|
||||
'Goodbye'
|
||||
];
|
||||
|
||||
// TODO: Create actual dialogue UI with choices
|
||||
console.log('Conversation options:', phrases);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zombie warning (important messages)
|
||||
*/
|
||||
zombieWarning(message, urgency = 'normal') {
|
||||
const urgencyIcons = {
|
||||
low: 'ℹ️',
|
||||
normal: '⚠️',
|
||||
high: '🚨',
|
||||
critical: '💀'
|
||||
};
|
||||
|
||||
this.showSubtitle({
|
||||
zombie: message,
|
||||
translation: message
|
||||
}, `${urgencyIcons[urgency]} ZOMBIE ALERT`);
|
||||
|
||||
// Play alert sound for high/critical
|
||||
if (urgency === 'high' || urgency === 'critical') {
|
||||
// TODO: Play alert sound
|
||||
this.scene.cameras.main.shake(200, 0.005);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Level up communication skill
|
||||
*/
|
||||
levelUpCommunication() {
|
||||
if (this.communicationLevel >= this.maxLevel) {
|
||||
console.log('🧠 Already at max level!');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setCommunicationLevel(this.communicationLevel + 1);
|
||||
|
||||
// Show what's unlocked
|
||||
if (this.communicationLevel === 5) {
|
||||
this.showNotification({
|
||||
title: 'Keywords Unlocked!',
|
||||
text: '🧠 You can now see KEYWORDS in zombie speech!',
|
||||
icon: '✨'
|
||||
});
|
||||
} else if (this.communicationLevel === 10) {
|
||||
this.showNotification({
|
||||
title: 'Full Understanding!',
|
||||
text: '🧠 You can now understand COMPLETE zombie sentences!',
|
||||
icon: '👑'
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get communication level
|
||||
*/
|
||||
getCommunicationLevel() {
|
||||
return this.communicationLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can understand zombie
|
||||
*/
|
||||
canUnderstandZombie(requiredLevel = 1) {
|
||||
return this.communicationLevel >= requiredLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Show notification
|
||||
*/
|
||||
showNotification(notification) {
|
||||
console.log(`📢 ${notification.icon} ${notification.title}: ${notification.text}`);
|
||||
|
||||
const ui = this.scene.scene.get('UIScene');
|
||||
if (ui && ui.showNotification) {
|
||||
ui.showNotification(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
569
DEBUG_TOTAL_RECOVERY/ZombieEconomySystem.js
Normal file
569
DEBUG_TOTAL_RECOVERY/ZombieEconomySystem.js
Normal file
@@ -0,0 +1,569 @@
|
||||
/**
|
||||
* ZOMBIE ECONOMY & CITY SANITATION SYSTEM
|
||||
* Mrtva Dolina - Worker Zombies, Smetarji, Redka Darila
|
||||
*
|
||||
* Features:
|
||||
* - Worker Zombies (heavy labor, construction)
|
||||
* - Sanitation System (Smetar cleans city)
|
||||
* - Rare Gift System (unique rewards from zombie work)
|
||||
* - Zombie Maintenance (Brain feeding)
|
||||
* - Contract System (loan zombies to NPCs)
|
||||
* - Happiness impact on city growth
|
||||
*/
|
||||
|
||||
export class ZombieEconomySystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Worker zombie types
|
||||
this.zombieTypes = {
|
||||
scout: {
|
||||
name: 'Zombi Skavt',
|
||||
strength: 30,
|
||||
speed: 60,
|
||||
brainConsumption: 0.5, // per hour
|
||||
tasks: ['scouting', 'light_work']
|
||||
},
|
||||
worker: {
|
||||
name: 'Delovni Zombi',
|
||||
strength: 80,
|
||||
speed: 40,
|
||||
brainConsumption: 1.5, // per hour
|
||||
tasks: ['construction', 'hauling', 'sanitation']
|
||||
},
|
||||
sanitation: {
|
||||
name: 'Smetar Zombi',
|
||||
strength: 50,
|
||||
speed: 50,
|
||||
brainConsumption: 1.0, // per hour
|
||||
tasks: ['cleaning', 'graffiti_removal']
|
||||
}
|
||||
};
|
||||
|
||||
// Active zombies
|
||||
this.activeZombies = [];
|
||||
|
||||
// Contracts (zombies loaned to NPCs)
|
||||
this.activeContracts = [];
|
||||
|
||||
// Rare gifts catalogue
|
||||
this.rareGifts = [
|
||||
{ id: 'family_heirloom', name: 'Starinski družinski artefakt', rarity: 'legendary', value: 1000 },
|
||||
{ id: 'ancient_dye', name: 'Starodavno barvilo', rarity: 'epic', value: 500 },
|
||||
{ id: 'rare_seed_pack', name: 'Paket redkih semen', rarity: 'rare', value: 300 },
|
||||
{ id: 'kai_memory_photo', name: 'Kaijeva skrita fotografija', rarity: 'legendary', value: 2000 },
|
||||
{ id: 'mystical_paint', name: 'Mistično barvilo', rarity: 'epic', value: 600 }
|
||||
];
|
||||
|
||||
// City cleanliness tracking
|
||||
this.cityTrash = [];
|
||||
this.cityGraffiti = [];
|
||||
this.cleanlinessScore = 50; // 0-100
|
||||
|
||||
// Happiness impact
|
||||
this.citizenHappiness = 50; // 0-100
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Listen for trash generation
|
||||
this.scene.events.on('npc:activity', this.onNPCActivity, this);
|
||||
this.scene.events.on('vandal:graffiti', this.onGraffitiCreated, this);
|
||||
|
||||
// Start passive systems
|
||||
this.startBrainConsumption();
|
||||
this.startCleaningSweep();
|
||||
|
||||
console.log('✅ ZombieEconomySystem initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* RECRUIT ZOMBIE
|
||||
*/
|
||||
recruitZombie(type, name = null) {
|
||||
const zombieData = this.zombieTypes[type];
|
||||
if (!zombieData) {
|
||||
console.error(`Unknown zombie type: ${type}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const zombie = {
|
||||
id: `zombie_${Date.now()}`,
|
||||
type: type,
|
||||
name: name || zombieData.name,
|
||||
strength: zombieData.strength,
|
||||
speed: zombieData.speed,
|
||||
brainLevel: 100, // 0-100, energy/hunger
|
||||
brainConsumption: zombieData.brainConsumption,
|
||||
tasks: zombieData.tasks,
|
||||
currentTask: null,
|
||||
isContracted: false,
|
||||
grumbleTimer: null,
|
||||
x: this.scene.player.x,
|
||||
y: this.scene.player.y,
|
||||
sprite: null
|
||||
};
|
||||
|
||||
// Create sprite
|
||||
zombie.sprite = this.scene.add.sprite(zombie.x, zombie.y, `zombie_${type}`);
|
||||
zombie.sprite.setDepth(5);
|
||||
|
||||
this.activeZombies.push(zombie);
|
||||
|
||||
console.log(`🧟 Recruited: ${zombie.name}`);
|
||||
|
||||
return zombie;
|
||||
}
|
||||
|
||||
/**
|
||||
* FEED ZOMBIE (with brains)
|
||||
*/
|
||||
feedZombie(zombieId, brainsAmount = 50) {
|
||||
const zombie = this.activeZombies.find(z => z.id === zombieId);
|
||||
if (!zombie) return false;
|
||||
|
||||
zombie.brainLevel = Math.min(100, zombie.brainLevel + brainsAmount);
|
||||
|
||||
// Happy zombie sound
|
||||
this.scene.sound.play('zombie_satisfied', { volume: 0.5 });
|
||||
|
||||
// Show feedback
|
||||
this.scene.events.emit('show-floating-text', {
|
||||
x: zombie.sprite.x,
|
||||
y: zombie.sprite.y - 30,
|
||||
text: '*munch* ...dobr!',
|
||||
color: '#90EE90'
|
||||
});
|
||||
|
||||
// Stop grumbling
|
||||
if (zombie.grumbleTimer) {
|
||||
clearInterval(zombie.grumbleTimer);
|
||||
zombie.grumbleTimer = null;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* BRAIN CONSUMPTION - Passive hunger system
|
||||
*/
|
||||
startBrainConsumption() {
|
||||
setInterval(() => {
|
||||
this.activeZombies.forEach(zombie => {
|
||||
// Reduce brain level based on consumption rate
|
||||
zombie.brainLevel = Math.max(0, zombie.brainLevel - (zombie.brainConsumption / 60)); // per minute
|
||||
|
||||
// Check hunger
|
||||
if (zombie.brainLevel < 30) {
|
||||
this.zombieHungry(zombie);
|
||||
}
|
||||
|
||||
// Critical hunger - zombie becomes slow
|
||||
if (zombie.brainLevel < 10) {
|
||||
zombie.speed *= 0.5; // 50% slower
|
||||
if (zombie.sprite) {
|
||||
zombie.sprite.setTint(0x666666); // Darken sprite
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 60000); // Every minute
|
||||
}
|
||||
|
||||
zombieHungry(zombie) {
|
||||
if (zombie.grumbleTimer) return; // Already grumbling
|
||||
|
||||
// Start periodic grumbling
|
||||
zombie.grumbleTimer = setInterval(() => {
|
||||
const hungerLines = [
|
||||
'Braaaaains...',
|
||||
'Možgaaaaani...',
|
||||
'Hrrrngh... lačen...',
|
||||
'*godrnja o hrani*'
|
||||
];
|
||||
|
||||
const line = Phaser.Utils.Array.GetRandom(hungerLines);
|
||||
|
||||
// Show speech bubble
|
||||
this.scene.events.emit('show-speech-bubble', {
|
||||
x: zombie.sprite.x,
|
||||
y: zombie.sprite.y - 50,
|
||||
text: line,
|
||||
duration: 3000
|
||||
});
|
||||
|
||||
// Play hungry sound
|
||||
if (this.scene.sound) {
|
||||
this.scene.sound.play('zombie_groan', { volume: 0.4 });
|
||||
}
|
||||
}, 15000); // Every 15 seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* CONTRACT SYSTEM - Loan zombies to NPCs
|
||||
*/
|
||||
createContract(zombieId, npc, task, duration, payment) {
|
||||
const zombie = this.activeZombies.find(z => z.id === zombieId);
|
||||
if (!zombie || zombie.isContracted) {
|
||||
console.warn('Zombie not available for contract');
|
||||
return false;
|
||||
}
|
||||
|
||||
const contract = {
|
||||
id: `contract_${Date.now()}`,
|
||||
zombieId: zombieId,
|
||||
npc: npc, // 'ivan_kovac', 'tehnik', etc.
|
||||
task: task, // 'wall_construction', 'steel_hauling', etc.
|
||||
duration: duration, // in game hours
|
||||
payment: payment, // gold or rare gift
|
||||
startTime: this.scene.gameTime || Date.now(),
|
||||
completed: false
|
||||
};
|
||||
|
||||
zombie.isContracted = true;
|
||||
zombie.currentTask = task;
|
||||
this.activeContracts.push(contract);
|
||||
|
||||
// Move zombie to work site
|
||||
this.moveZombieToWorkSite(zombie, npc);
|
||||
|
||||
console.log(`📄 Contract created: ${zombie.name} → ${npc} (${task})`);
|
||||
|
||||
// Start work visuals
|
||||
this.startWorkAnimation(zombie, task);
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
moveZombieToWorkSite(zombie, npc) {
|
||||
const workSites = {
|
||||
ivan_kovac: { x: 300, y: 200 }, // Blacksmith
|
||||
tehnik: { x: 500, y: 250 }, // Tech Workshop
|
||||
mayor: { x: 400, y: 300 } // Town Hall
|
||||
};
|
||||
|
||||
const site = workSites[npc] || { x: 400, y: 300 };
|
||||
|
||||
if (zombie.sprite) {
|
||||
this.scene.tweens.add({
|
||||
targets: zombie.sprite,
|
||||
x: site.x,
|
||||
y: site.y,
|
||||
duration: 2000,
|
||||
ease: 'Sine.easeInOut'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startWorkAnimation(zombie, task) {
|
||||
// Different animations for different tasks
|
||||
const animations = {
|
||||
wall_construction: 'zombie_hammering',
|
||||
steel_hauling: 'zombie_carrying',
|
||||
sanitation: 'zombie_sweeping',
|
||||
graffiti_removal: 'zombie_scrubbing'
|
||||
};
|
||||
|
||||
const anim = animations[task] || 'zombie_working';
|
||||
|
||||
if (zombie.sprite && zombie.sprite.anims) {
|
||||
zombie.sprite.play(anim, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* COMPLETE CONTRACT - Award payment
|
||||
*/
|
||||
completeContract(contractId) {
|
||||
const contract = this.activeContracts.find(c => c.id === contractId);
|
||||
if (!contract || contract.completed) return;
|
||||
|
||||
const zombie = this.activeZombies.find(z => z.id === contract.zombieId);
|
||||
|
||||
contract.completed = true;
|
||||
zombie.isContracted = false;
|
||||
zombie.currentTask = null;
|
||||
|
||||
// Award payment
|
||||
if (contract.payment.type === 'gold') {
|
||||
this.scene.inventorySystem.addGold(contract.payment.amount);
|
||||
|
||||
this.scene.events.emit('show-notification', {
|
||||
title: 'Pogodba Končana',
|
||||
message: `${zombie.name} je končal delo! +${contract.payment.amount} zlata.`,
|
||||
icon: '💰',
|
||||
duration: 3000
|
||||
});
|
||||
} else if (contract.payment.type === 'rare_gift') {
|
||||
this.awardRareGift(contract.payment.giftId);
|
||||
}
|
||||
|
||||
// Return zombie to player
|
||||
if (zombie.sprite) {
|
||||
this.scene.tweens.add({
|
||||
targets: zombie.sprite,
|
||||
x: this.scene.player.x + 50,
|
||||
y: this.scene.player.y,
|
||||
duration: 2000,
|
||||
ease: 'Sine.easeOut'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ Contract completed: ${contract.task}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* RARE GIFT SYSTEM
|
||||
*/
|
||||
awardRareGift(giftId = null) {
|
||||
// Random gift if not specified
|
||||
if (!giftId) {
|
||||
const gift = Phaser.Utils.Array.GetRandom(this.rareGifts);
|
||||
giftId = gift.id;
|
||||
}
|
||||
|
||||
const gift = this.rareGifts.find(g => g.id === giftId);
|
||||
if (!gift) return;
|
||||
|
||||
// Add to inventory
|
||||
if (this.scene.inventorySystem) {
|
||||
this.scene.inventorySystem.addItem(gift.id, 1);
|
||||
}
|
||||
|
||||
// Show special notification
|
||||
this.scene.events.emit('show-notification', {
|
||||
title: '🎁 REDKO DARILO!',
|
||||
message: `Prejel si: ${gift.name} (${gift.rarity})`,
|
||||
icon: '✨',
|
||||
duration: 7000,
|
||||
color: this.getRarityColor(gift.rarity)
|
||||
});
|
||||
|
||||
// Play special sound
|
||||
if (this.scene.sound) {
|
||||
this.scene.sound.play('rare_gift_fanfare', { volume: 0.7 });
|
||||
}
|
||||
|
||||
console.log(`🎁 Awarded rare gift: ${gift.name}`);
|
||||
}
|
||||
|
||||
getRarityColor(rarity) {
|
||||
const colors = {
|
||||
legendary: '#FFD700', // Gold
|
||||
epic: '#9B59B6', // Purple
|
||||
rare: '#3498DB' // Blue
|
||||
};
|
||||
return colors[rarity] || '#FFFFFF';
|
||||
}
|
||||
|
||||
/**
|
||||
* SANITATION SYSTEM - Trash & Graffiti
|
||||
*/
|
||||
onNPCActivity(npcData) {
|
||||
// NPCs randomly drop trash
|
||||
if (Math.random() < 0.1) { // 10% chance
|
||||
this.spawnTrash(npcData.x, npcData.y);
|
||||
}
|
||||
}
|
||||
|
||||
spawnTrash(x, y) {
|
||||
const trashTypes = ['trash_bag', 'litter', 'broken_bottle', 'paper_waste'];
|
||||
const type = Phaser.Utils.Array.GetRandom(trashTypes);
|
||||
|
||||
const trash = this.scene.add.sprite(x, y, type);
|
||||
trash.setDepth(1);
|
||||
|
||||
this.cityTrash.push({
|
||||
id: `trash_${Date.now()}_${Math.random()}`,
|
||||
x: x,
|
||||
y: y,
|
||||
type: type,
|
||||
sprite: trash
|
||||
});
|
||||
|
||||
// Lower cleanliness
|
||||
this.cleanlinessScore = Math.max(0, this.cleanlinessScore - 2);
|
||||
this.updateCityStats();
|
||||
}
|
||||
|
||||
onGraffitiCreated(graffitiData) {
|
||||
const graffiti = this.scene.add.sprite(graffitiData.x, graffitiData.y, 'graffiti_tag');
|
||||
graffiti.setDepth(2);
|
||||
|
||||
this.cityGraffiti.push({
|
||||
id: `graffiti_${Date.now()}`,
|
||||
x: graffitiData.x,
|
||||
y: graffitiData.y,
|
||||
sprite: graffiti
|
||||
});
|
||||
|
||||
// Lower cleanliness
|
||||
this.cleanlinessScore = Math.max(0, this.cleanlinessScore - 5);
|
||||
this.updateCityStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* CLEANING SWEEP - Zombies clean autonomously
|
||||
*/
|
||||
startCleaningSweep() {
|
||||
setInterval(() => {
|
||||
// Find sanitation zombies
|
||||
const cleaners = this.activeZombies.filter(z =>
|
||||
z.tasks.includes('cleaning') &&
|
||||
!z.isContracted &&
|
||||
z.brainLevel > 20
|
||||
);
|
||||
|
||||
cleaners.forEach(cleaner => {
|
||||
// Clean nearest trash
|
||||
const nearestTrash = this.findNearestTrash(cleaner.sprite.x, cleaner.sprite.y);
|
||||
if (nearestTrash) {
|
||||
this.cleanTrash(cleaner, nearestTrash);
|
||||
}
|
||||
|
||||
// Clean nearest graffiti
|
||||
const nearestGraffiti = this.findNearestGraffiti(cleaner.sprite.x, cleaner.sprite.y);
|
||||
if (nearestGraffiti) {
|
||||
this.cleanGraffiti(cleaner, nearestGraffiti);
|
||||
}
|
||||
});
|
||||
}, 10000); // Every 10 seconds
|
||||
}
|
||||
|
||||
findNearestTrash(x, y) {
|
||||
let nearest = null;
|
||||
let minDist = Infinity;
|
||||
|
||||
this.cityTrash.forEach(trash => {
|
||||
const dist = Phaser.Math.Distance.Between(x, y, trash.x, trash.y);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearest = trash;
|
||||
}
|
||||
});
|
||||
|
||||
return minDist < 200 ? nearest : null; // Within 200px
|
||||
}
|
||||
|
||||
findNearestGraffiti(x, y) {
|
||||
let nearest = null;
|
||||
let minDist = Infinity;
|
||||
|
||||
this.cityGraffiti.forEach(graffiti => {
|
||||
const dist = Phaser.Math.Distance.Between(x, y, graffiti.x, graffiti.y);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearest = graffiti;
|
||||
}
|
||||
});
|
||||
|
||||
return minDist < 200 ? nearest : null;
|
||||
}
|
||||
|
||||
cleanTrash(zombie, trash) {
|
||||
// Move to trash
|
||||
this.scene.tweens.add({
|
||||
targets: zombie.sprite,
|
||||
x: trash.x,
|
||||
y: trash.y,
|
||||
duration: 1000,
|
||||
onComplete: () => {
|
||||
// Play cleaning animation
|
||||
if (zombie.sprite.anims) {
|
||||
zombie.sprite.play('zombie_sweeping', true);
|
||||
}
|
||||
|
||||
// Remove trash after delay
|
||||
setTimeout(() => {
|
||||
trash.sprite.destroy();
|
||||
this.cityTrash = this.cityTrash.filter(t => t.id !== trash.id);
|
||||
|
||||
// Increase cleanliness
|
||||
this.cleanlinessScore = Math.min(100, this.cleanlinessScore + 3);
|
||||
this.updateCityStats();
|
||||
|
||||
console.log(`🧹 ${zombie.name} cleaned trash`);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cleanGraffiti(zombie, graffiti) {
|
||||
// Move to graffiti
|
||||
this.scene.tweens.add({
|
||||
targets: zombie.sprite,
|
||||
x: graffiti.x,
|
||||
y: graffiti.y,
|
||||
duration: 1000,
|
||||
onComplete: () => {
|
||||
// Play scrubbing animation
|
||||
if (zombie.sprite.anims) {
|
||||
zombie.sprite.play('zombie_scrubbing', true);
|
||||
}
|
||||
|
||||
// Remove graffiti with fade
|
||||
this.scene.tweens.add({
|
||||
targets: graffiti.sprite,
|
||||
alpha: 0,
|
||||
duration: 3000,
|
||||
onComplete: () => {
|
||||
graffiti.sprite.destroy();
|
||||
this.cityGraffiti = this.cityGraffiti.filter(g => g.id !== graffiti.id);
|
||||
|
||||
// Increase cleanliness
|
||||
this.cleanlinessScore = Math.min(100, this.cleanlinessScore + 5);
|
||||
this.updateCityStats();
|
||||
|
||||
console.log(`🧽 ${zombie.name} removed graffiti`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CITY STATS UPDATE
|
||||
*/
|
||||
updateCityStats() {
|
||||
// Calculate happiness based on cleanliness
|
||||
this.citizenHappiness = Math.min(100, this.cleanlinessScore * 1.2);
|
||||
|
||||
// Emit stats update
|
||||
this.scene.events.emit('city:stats_updated', {
|
||||
cleanliness: this.cleanlinessScore,
|
||||
happiness: this.citizenHappiness,
|
||||
trashCount: this.cityTrash.length,
|
||||
graffitiCount: this.cityGraffiti.length
|
||||
});
|
||||
|
||||
// Happiness affects NPC arrival rate
|
||||
if (this.citizenHappiness > 70) {
|
||||
// Faster settler arrivals
|
||||
this.scene.events.emit('city:boost_immigration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get zombie status for UI
|
||||
*/
|
||||
getZombieStatus() {
|
||||
return this.activeZombies.map(z => ({
|
||||
id: z.id,
|
||||
name: z.name,
|
||||
type: z.type,
|
||||
brainLevel: z.brainLevel,
|
||||
currentTask: z.currentTask,
|
||||
isContracted: z.isContracted
|
||||
}));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.activeZombies.forEach(zombie => {
|
||||
if (zombie.sprite) zombie.sprite.destroy();
|
||||
if (zombie.grumbleTimer) clearInterval(zombie.grumbleTimer);
|
||||
});
|
||||
|
||||
this.cityTrash.forEach(trash => trash.sprite.destroy());
|
||||
this.cityGraffiti.forEach(graffiti => graffiti.sprite.destroy());
|
||||
}
|
||||
}
|
||||
464
DEBUG_TOTAL_RECOVERY/ZombieMinerAutomationSystem.js
Normal file
464
DEBUG_TOTAL_RECOVERY/ZombieMinerAutomationSystem.js
Normal file
@@ -0,0 +1,464 @@
|
||||
/**
|
||||
* ZOMBIE MINER AUTOMATION SYSTEM
|
||||
* Part of: Mining System Expansion
|
||||
* Created: January 4, 2026
|
||||
*
|
||||
* Features:
|
||||
* - Hire zombie miners for passive resource generation
|
||||
* - Assign miners to specific mine depths
|
||||
* - Efficiency & loyalty mechanics
|
||||
* - Automated ore collection
|
||||
* - Zombie equipment upgrades
|
||||
*/
|
||||
|
||||
class ZombieMinerAutomationSystem {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.player = game.player;
|
||||
|
||||
// Zombie miners
|
||||
this.zombieMiners = [];
|
||||
this.maxZombieMiners = 10;
|
||||
this.zombieMinerCost = 5000;
|
||||
|
||||
// Automation settings
|
||||
this.automationActive = false;
|
||||
this.totalYieldPerHour = 0;
|
||||
this.lastCollectionTime = null;
|
||||
|
||||
// Equipment for zombies
|
||||
this.zombieEquipment = {
|
||||
pickaxe_tier: 1, // 1-5
|
||||
helmet_lamp: false, // Better visibility
|
||||
oxygen_tank: false, // Deeper mining
|
||||
cart: false // Auto-transport
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hire a zombie miner
|
||||
*/
|
||||
hireZombieMiner() {
|
||||
if (this.zombieMiners.length >= this.maxZombieMiners) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Maximum ${this.maxZombieMiners} zombie miners allowed!`
|
||||
};
|
||||
}
|
||||
|
||||
if (this.player.money < this.zombieMinerCost) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Need ${this.zombieMinerCost}g to hire zombie miner!`
|
||||
};
|
||||
}
|
||||
|
||||
// Hire zombie
|
||||
this.player.money -= this.zombieMinerCost;
|
||||
|
||||
const miner = {
|
||||
id: `zombie_miner_${this.zombieMiners.length + 1}`,
|
||||
name: this.generateZombieName(),
|
||||
assignedMine: null,
|
||||
assignedDepth: 0,
|
||||
efficiency: 1.0, // 0.5 - 2.0
|
||||
loyalty: 50, // 0-100
|
||||
yieldPerHour: 5, // Base yield
|
||||
level: 1,
|
||||
experience: 0
|
||||
};
|
||||
|
||||
this.zombieMiners.push(miner);
|
||||
|
||||
// Recalculate automation
|
||||
this.updateAutomationYield();
|
||||
|
||||
this.game.showMessage(
|
||||
`Hired ${miner.name}! (${this.zombieMiners.length}/${this.maxZombieMiners})`
|
||||
);
|
||||
|
||||
return { success: true, miner: miner };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random zombie miner name
|
||||
*/
|
||||
generateZombieName() {
|
||||
const prefixes = ['Grumpy', 'Rusty', 'Dusty', 'Grumbly', 'Moaning', 'Shuffling'];
|
||||
const suffixes = ['Zed', 'Mort', 'Bones', 'Guts', 'Picks', 'Drills'];
|
||||
|
||||
const prefix = Phaser.Utils.Array.GetRandom(prefixes);
|
||||
const suffix = Phaser.Utils.Array.GetRandom(suffixes);
|
||||
|
||||
return `${prefix} ${suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign zombie miner to specific mine & depth
|
||||
*/
|
||||
assignZombieMiner(minerId, mineId, depth) {
|
||||
const miner = this.zombieMiners.find(m => m.id === minerId);
|
||||
|
||||
if (!miner) {
|
||||
return { success: false, message: 'Zombie miner not found!' };
|
||||
}
|
||||
|
||||
// Check if mine exists
|
||||
const mine = this.game.miningSystem.getMineInfo(mineId);
|
||||
if (!mine) {
|
||||
return { success: false, message: 'Mine not found!' };
|
||||
}
|
||||
|
||||
// Check if depth is unlocked
|
||||
const maxDepth = this.game.miningSystem.maxDepthReached || 0;
|
||||
if (depth > maxDepth) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Must explore depth ${depth} first!`
|
||||
};
|
||||
}
|
||||
|
||||
// Assign miner
|
||||
miner.assignedMine = mineId;
|
||||
miner.assignedDepth = depth;
|
||||
|
||||
// Update automation
|
||||
this.updateAutomationYield();
|
||||
|
||||
this.game.showMessage(
|
||||
`${miner.name} assigned to ${mine.name} - Depth ${depth}`
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Unassign zombie miner (return to surface)
|
||||
*/
|
||||
unassignZombieMiner(minerId) {
|
||||
const miner = this.zombieMiners.find(m => m.id === minerId);
|
||||
|
||||
if (!miner) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
miner.assignedMine = null;
|
||||
miner.assignedDepth = 0;
|
||||
|
||||
// Update automation
|
||||
this.updateAutomationYield();
|
||||
|
||||
this.game.showMessage(
|
||||
`${miner.name} returned to surface.`
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update total automation yield
|
||||
*/
|
||||
updateAutomationYield() {
|
||||
let totalYield = 0;
|
||||
|
||||
this.zombieMiners.forEach(miner => {
|
||||
if (miner.assignedMine && miner.assignedDepth > 0) {
|
||||
// Base yield
|
||||
let hourlyYield = miner.yieldPerHour;
|
||||
|
||||
// Depth bonus (+10% per 10 levels)
|
||||
const depthBonus = (miner.assignedDepth / 10) * 0.1;
|
||||
hourlyYield *= (1 + depthBonus);
|
||||
|
||||
// Efficiency factor
|
||||
hourlyYield *= miner.efficiency;
|
||||
|
||||
// Loyalty factor (50% loyalty = 0.5x yield, 100% = 1.5x yield)
|
||||
const loyaltyFactor = 0.5 + (miner.loyalty / 100);
|
||||
hourlyYield *= loyaltyFactor;
|
||||
|
||||
// Equipment bonuses
|
||||
if (this.zombieEquipment.pickaxe_tier > 1) {
|
||||
hourlyYield *= (1 + (this.zombieEquipment.pickaxe_tier - 1) * 0.25);
|
||||
}
|
||||
if (this.zombieEquipment.cart) {
|
||||
hourlyYield *= 1.5; // 50% faster collection
|
||||
}
|
||||
|
||||
totalYield += hourlyYield;
|
||||
}
|
||||
});
|
||||
|
||||
this.totalYieldPerHour = Math.floor(totalYield);
|
||||
this.automationActive = (totalYield > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect automated mining resources
|
||||
*/
|
||||
collectAutomatedYield() {
|
||||
if (!this.automationActive) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No zombie miners assigned!'
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate time since last collection
|
||||
const hoursSinceLastCollection = this.getHoursSinceLastCollection();
|
||||
|
||||
if (hoursSinceLastCollection < 0.1) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Collected too recently! Wait a bit.'
|
||||
};
|
||||
}
|
||||
|
||||
const resources = {};
|
||||
|
||||
// Collect from each zombie
|
||||
this.zombieMiners.forEach(miner => {
|
||||
if (miner.assignedMine && miner.assignedDepth > 0) {
|
||||
// Get mine info
|
||||
const mine = this.game.miningSystem.getMineInfo(miner.assignedMine);
|
||||
|
||||
// Determine ore type based on depth
|
||||
const oreType = this.getOreTypeForDepth(mine, miner.assignedDepth);
|
||||
|
||||
// Calculate yield
|
||||
const hourlyYield = miner.yieldPerHour * miner.efficiency *
|
||||
(0.5 + miner.loyalty / 100);
|
||||
|
||||
const amount = Math.floor(hourlyYield * hoursSinceLastCollection);
|
||||
|
||||
if (amount > 0) {
|
||||
resources[oreType] = (resources[oreType] || 0) + amount;
|
||||
|
||||
// Grant XP to miner
|
||||
miner.experience += amount;
|
||||
this.checkMinerLevelUp(miner);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add resources to inventory
|
||||
let totalCollected = 0;
|
||||
for (const [ore, amount] of Object.entries(resources)) {
|
||||
this.player.inventory.addItem(ore, amount);
|
||||
totalCollected += amount;
|
||||
}
|
||||
|
||||
// Update last collection time
|
||||
this.lastCollectionTime = this.game.time.currentTime;
|
||||
|
||||
// Show collection message
|
||||
const resourceList = Object.entries(resources)
|
||||
.map(([ore, amount]) => `${amount}x ${ore}`)
|
||||
.join(', ');
|
||||
|
||||
this.game.showMessage(
|
||||
`Collected: ${resourceList} (${hoursSinceLastCollection.toFixed(1)}h)`
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
resources: resources,
|
||||
totalAmount: totalCollected,
|
||||
hours: hoursSinceLastCollection
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ore type based on mine and depth
|
||||
*/
|
||||
getOreTypeForDepth(mine, depth) {
|
||||
// Logic from mine zones
|
||||
if (depth <= 25) return 'copper_ore';
|
||||
if (depth <= 50) return 'iron_ore';
|
||||
if (depth <= 75) return 'gold_ore';
|
||||
return 'diamond_ore';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if miner levels up
|
||||
*/
|
||||
checkMinerLevelUp(miner) {
|
||||
const xpRequired = miner.level * 100;
|
||||
|
||||
if (miner.experience >= xpRequired) {
|
||||
miner.level++;
|
||||
miner.experience = 0;
|
||||
|
||||
// Bonuses per level
|
||||
miner.yieldPerHour += 1;
|
||||
miner.efficiency += 0.05;
|
||||
|
||||
this.game.showMessage(
|
||||
`${miner.name} leveled up! (Lv.${miner.level})`
|
||||
);
|
||||
|
||||
this.updateAutomationYield();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed zombie miner (restore loyalty)
|
||||
*/
|
||||
feedZombieMiner(minerId, foodType) {
|
||||
const miner = this.zombieMiners.find(m => m.id === minerId);
|
||||
|
||||
if (!miner) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// Check if player has food
|
||||
if (!this.player.inventory.hasItem(foodType)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'You don\'t have this food!'
|
||||
};
|
||||
}
|
||||
|
||||
// Remove food
|
||||
this.player.inventory.removeItem(foodType, 1);
|
||||
|
||||
// Loyalty bonus based on food quality
|
||||
const loyaltyGain = this.getFoodLoyaltyValue(foodType);
|
||||
miner.loyalty = Math.min(100, miner.loyalty + loyaltyGain);
|
||||
|
||||
this.game.showMessage(
|
||||
`${miner.name} ate ${foodType}. Loyalty +${loyaltyGain} (${miner.loyalty}/100)`
|
||||
);
|
||||
|
||||
// Update automation
|
||||
this.updateAutomationYield();
|
||||
|
||||
return { success: true, loyaltyGain: loyaltyGain };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get food loyalty value
|
||||
*/
|
||||
getFoodLoyaltyValue(foodType) {
|
||||
const foodValues = {
|
||||
'brain': 20, // Best food
|
||||
'meat': 15,
|
||||
'bread': 10,
|
||||
'vegetables': 5
|
||||
};
|
||||
|
||||
return foodValues[foodType] || 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade zombie equipment
|
||||
*/
|
||||
upgradeEquipment(equipmentType) {
|
||||
const costs = {
|
||||
pickaxe_tier: 3000,
|
||||
helmet_lamp: 2000,
|
||||
oxygen_tank: 2500,
|
||||
cart: 4000
|
||||
};
|
||||
|
||||
const cost = costs[equipmentType];
|
||||
|
||||
if (!cost) {
|
||||
return { success: false, message: 'Invalid equipment!' };
|
||||
}
|
||||
|
||||
// Check if already owned (except pickaxe tiers)
|
||||
if (equipmentType !== 'pickaxe_tier') {
|
||||
if (this.zombieEquipment[equipmentType]) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Already owned!'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Check pickaxe tier limit
|
||||
if (this.zombieEquipment.pickaxe_tier >= 5) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Pickaxe already max tier!'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check money
|
||||
if (this.player.money < cost) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Need ${cost}g!`
|
||||
};
|
||||
}
|
||||
|
||||
// Purchase
|
||||
this.player.money -= cost;
|
||||
|
||||
if (equipmentType === 'pickaxe_tier') {
|
||||
this.zombieEquipment.pickaxe_tier++;
|
||||
} else {
|
||||
this.zombieEquipment[equipmentType] = true;
|
||||
}
|
||||
|
||||
// Update automation
|
||||
this.updateAutomationYield();
|
||||
|
||||
this.game.showMessage(
|
||||
`Upgraded zombie equipment: ${equipmentType}! ${cost}g`
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hours since last collection
|
||||
*/
|
||||
getHoursSinceLastCollection() {
|
||||
if (!this.lastCollectionTime) {
|
||||
this.lastCollectionTime = this.game.time.currentTime;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const secondsElapsed = this.game.time.currentTime - this.lastCollectionTime;
|
||||
return secondsElapsed / 3600;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get automation UI data
|
||||
*/
|
||||
getAutomationUIData() {
|
||||
return {
|
||||
zombieMiners: this.zombieMiners,
|
||||
maxZombieMiners: this.maxZombieMiners,
|
||||
totalYieldPerHour: this.totalYieldPerHour,
|
||||
automationActive: this.automationActive,
|
||||
equipment: this.zombieEquipment,
|
||||
canHireMore: this.zombieMiners.length < this.maxZombieMiners,
|
||||
hireCost: this.zombieMinerCost,
|
||||
hoursSinceCollection: this.getHoursSinceLastCollection()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update (passive decay of loyalty)
|
||||
*/
|
||||
update(deltaTime) {
|
||||
// Loyalty slowly decays if zombies are working
|
||||
this.zombieMiners.forEach(miner => {
|
||||
if (miner.assignedMine && miner.assignedDepth > 0) {
|
||||
// Lose 1 loyalty per hour worked
|
||||
const loyaltyLoss = (deltaTime / 3600);
|
||||
miner.loyalty = Math.max(0, miner.loyalty - loyaltyLoss);
|
||||
}
|
||||
});
|
||||
|
||||
// Recalculate if needed
|
||||
if (this.automationActive) {
|
||||
this.updateAutomationYield();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
340
DEBUG_TOTAL_RECOVERY/ZombieScoutLevelingSystem.js
Normal file
340
DEBUG_TOTAL_RECOVERY/ZombieScoutLevelingSystem.js
Normal file
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* ZOMBIE SCOUT LEVELING SYSTEM (1-20)
|
||||
* XP progression, stat increases, level-up rewards
|
||||
* Integrates with ZombieScout companion
|
||||
*/
|
||||
|
||||
export class ZombieScoutLevelingSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Current stats
|
||||
this.currentLevel = 1;
|
||||
this.currentXP = 0;
|
||||
this.skillPoints = 0;
|
||||
|
||||
// Stats per level
|
||||
this.baseStats = {
|
||||
health: 50,
|
||||
attack: 5,
|
||||
digSpeed: 1.0,
|
||||
moveSpeed: 100,
|
||||
defense: 0
|
||||
};
|
||||
|
||||
this.currentStats = { ...this.baseStats };
|
||||
|
||||
// XP curve
|
||||
this.xpRequirements = this.generateXPCurve();
|
||||
|
||||
// Level rewards
|
||||
this.levelRewards = new Map();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initializeLevelRewards();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate XP requirements for each level (exponential curve)
|
||||
*/
|
||||
generateXPCurve() {
|
||||
const curve = [0]; // Level 1 = 0 XP
|
||||
|
||||
for (let level = 2; level <= 20; level++) {
|
||||
// Formula: baseXP * (level ^ 1.5)
|
||||
const baseXP = 100;
|
||||
const requiredXP = Math.floor(baseXP * Math.pow(level, 1.5));
|
||||
curve.push(requiredXP);
|
||||
}
|
||||
|
||||
return curve;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize rewards for each level
|
||||
*/
|
||||
initializeLevelRewards() {
|
||||
// Skill points every level
|
||||
for (let level = 2; level <= 20; level++) {
|
||||
this.levelRewards.set(level, {
|
||||
skillPoints: 1,
|
||||
statBonus: this.getStatBonusForLevel(level),
|
||||
specialUnlock: this.getSpecialUnlockForLevel(level)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stat bonus for specific level
|
||||
*/
|
||||
getStatBonusForLevel(level) {
|
||||
const bonuses = {
|
||||
health: 10, // +10 HP per level
|
||||
attack: 1, // +1 attack per level
|
||||
digSpeed: 0.05, // +5% dig speed per level
|
||||
defense: 0.5 // +0.5 defense per level
|
||||
};
|
||||
|
||||
// Every 5 levels: bonus move speed
|
||||
if (level % 5 === 0) {
|
||||
bonuses.moveSpeed = 10; // +10 speed at levels 5, 10, 15, 20
|
||||
}
|
||||
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get special unlock for milestone levels
|
||||
*/
|
||||
getSpecialUnlockForLevel(level) {
|
||||
const unlocks = {
|
||||
5: { type: 'skill', name: 'Basic Attack', description: 'Zombie can attack enemies' },
|
||||
10: { type: 'skill', name: 'Speed Dig', description: 'Dig 50% faster for 10 seconds' },
|
||||
15: { type: 'skill', name: 'Treasure Sense', description: 'Reveal nearby buried items' },
|
||||
20: { type: 'evolution', name: 'Scout Commander', description: 'Zombie becomes elite unit' }
|
||||
};
|
||||
|
||||
return unlocks[level] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Award XP to Zombie Scout
|
||||
*/
|
||||
awardXP(amount, source = 'general') {
|
||||
this.currentXP += amount;
|
||||
|
||||
// Visual feedback
|
||||
this.showXPGain(amount, source);
|
||||
|
||||
// Check for level up
|
||||
this.checkLevelUp();
|
||||
|
||||
console.log(`+${amount} XP from ${source} (${this.currentXP}/${this.getRequiredXP()})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Zombie Scout should level up
|
||||
*/
|
||||
checkLevelUp() {
|
||||
if (this.currentLevel >= 20) return; // Max level cap
|
||||
|
||||
const requiredXP = this.getRequiredXP();
|
||||
|
||||
if (this.currentXP >= requiredXP) {
|
||||
this.levelUp();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Level up Zombie Scout
|
||||
*/
|
||||
levelUp() {
|
||||
this.currentLevel++;
|
||||
this.currentXP -= this.getRequiredXP(this.currentLevel - 1); // Carry over excess XP
|
||||
|
||||
// Apply stat bonuses
|
||||
const rewards = this.levelRewards.get(this.currentLevel);
|
||||
if (rewards) {
|
||||
// Apply stat increases
|
||||
for (const [stat, bonus] of Object.entries(rewards.statBonus)) {
|
||||
this.currentStats[stat] += bonus;
|
||||
}
|
||||
|
||||
// Award skill points
|
||||
this.skillPoints += rewards.skillPoints;
|
||||
|
||||
// Special unlock
|
||||
if (rewards.specialUnlock) {
|
||||
this.unlockSpecial(rewards.specialUnlock);
|
||||
}
|
||||
}
|
||||
|
||||
// Visual effects
|
||||
this.playLevelUpEffects();
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`Zombie Scout reached Level ${this.currentLevel}!`,
|
||||
'level_up',
|
||||
{ skillPoints: this.skillPoints }
|
||||
);
|
||||
|
||||
// Update Zombie Scout sprite if evolution
|
||||
if (this.currentLevel === 20) {
|
||||
this.evolveScout();
|
||||
}
|
||||
|
||||
console.log(`🎉 LEVEL UP! Zombie Scout is now Level ${this.currentLevel}`);
|
||||
console.log(`Stats:`, this.currentStats);
|
||||
console.log(`Skill Points: ${this.skillPoints}`);
|
||||
|
||||
// Quest check
|
||||
this.scene.questSystem?.checkScoutLevel(this.currentLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock special ability or feature
|
||||
*/
|
||||
unlockSpecial(unlock) {
|
||||
console.log(`✨ Special Unlock: ${unlock.name} - ${unlock.description}`);
|
||||
|
||||
if (unlock.type === 'skill') {
|
||||
this.scene.zombieScoutSkills?.unlockSkill(unlock.name);
|
||||
} else if (unlock.type === 'evolution') {
|
||||
// Mark for visual evolution
|
||||
this.isEvolved = true;
|
||||
}
|
||||
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`Unlocked: ${unlock.name}!`,
|
||||
'unlock',
|
||||
{ description: unlock.description }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual evolution at level 20
|
||||
*/
|
||||
evolveScout() {
|
||||
// Update sprite to "Commander" version
|
||||
const scout = this.scene.zombieScout;
|
||||
if (scout) {
|
||||
scout.setTexture('zombie_scout_commander');
|
||||
scout.setScale(scout.scale * 1.2); // Slightly larger
|
||||
|
||||
// VFX: Evolution animation
|
||||
this.scene.vfxSystem?.playEffect('evolution', scout.x, scout.y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Award XP for different actions
|
||||
*/
|
||||
onCombatKill(enemyType) {
|
||||
const xpValues = {
|
||||
'zombie': 10,
|
||||
'raider': 25,
|
||||
'mutant': 50,
|
||||
'boss': 200
|
||||
};
|
||||
|
||||
this.awardXP(xpValues[enemyType] || 10, 'combat');
|
||||
}
|
||||
|
||||
onDigComplete(itemRarity) {
|
||||
const xpValues = {
|
||||
'common': 5,
|
||||
'uncommon': 15,
|
||||
'rare': 30,
|
||||
'legendary': 100
|
||||
};
|
||||
|
||||
this.awardXP(xpValues[itemRarity] || 5, 'digging');
|
||||
}
|
||||
|
||||
onExplorationProgress() {
|
||||
// Award XP for exploring new areas
|
||||
this.awardXP(20, 'exploration');
|
||||
}
|
||||
|
||||
onQuestComplete(questDifficulty) {
|
||||
const xpValues = {
|
||||
'easy': 50,
|
||||
'medium': 100,
|
||||
'hard': 200,
|
||||
'story': 300
|
||||
};
|
||||
|
||||
this.awardXP(xpValues[questDifficulty] || 50, 'quest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get required XP for current or specific level
|
||||
*/
|
||||
getRequiredXP(level = this.currentLevel) {
|
||||
return this.xpRequirements[level] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get XP progress percentage
|
||||
*/
|
||||
getXPProgress() {
|
||||
if (this.currentLevel >= 20) return 100;
|
||||
|
||||
const required = this.getRequiredXP();
|
||||
return Math.min(100, Math.floor((this.currentXP / required) * 100));
|
||||
}
|
||||
|
||||
/**
|
||||
* Visual effects
|
||||
*/
|
||||
showXPGain(amount, source) {
|
||||
// Floating text above Zombie Scout
|
||||
const scout = this.scene.zombieScout;
|
||||
if (scout) {
|
||||
this.scene.vfxSystem?.floatingText(`+${amount} XP`, scout.x, scout.y - 30, '#FFD700');
|
||||
}
|
||||
}
|
||||
|
||||
playLevelUpEffects() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (!scout) return;
|
||||
|
||||
// VFX: Level up particles
|
||||
this.scene.vfxSystem?.playEffect('level_up', scout.x, scout.y);
|
||||
|
||||
// SFX: Level up sound
|
||||
this.scene.soundSystem?.play('level_up');
|
||||
|
||||
// Camera shake
|
||||
this.scene.cameras.main.shake(200, 0.005);
|
||||
|
||||
// Flash
|
||||
this.scene.cameras.main.flash(300, 255, 215, 0); // Gold flash
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current level data
|
||||
*/
|
||||
getLevelData() {
|
||||
return {
|
||||
level: this.currentLevel,
|
||||
xp: this.currentXP,
|
||||
requiredXP: this.getRequiredXP(),
|
||||
progress: this.getXPProgress(),
|
||||
stats: { ...this.currentStats },
|
||||
skillPoints: this.skillPoints,
|
||||
isMaxLevel: this.currentLevel >= 20,
|
||||
nextUnlock: this.levelRewards.get(this.currentLevel + 1)?.specialUnlock || null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save/Load support
|
||||
*/
|
||||
getSaveData() {
|
||||
return {
|
||||
currentLevel: this.currentLevel,
|
||||
currentXP: this.currentXP,
|
||||
skillPoints: this.skillPoints,
|
||||
currentStats: this.currentStats,
|
||||
isEvolved: this.isEvolved || false
|
||||
};
|
||||
}
|
||||
|
||||
loadSaveData(data) {
|
||||
this.currentLevel = data.currentLevel || 1;
|
||||
this.currentXP = data.currentXP || 0;
|
||||
this.skillPoints = data.skillPoints || 0;
|
||||
this.currentStats = data.currentStats || { ...this.baseStats };
|
||||
this.isEvolved = data.isEvolved || false;
|
||||
|
||||
// Apply visual evolution if loaded at level 20
|
||||
if (this.isEvolved) {
|
||||
this.evolveScout();
|
||||
}
|
||||
}
|
||||
}
|
||||
420
DEBUG_TOTAL_RECOVERY/ZombieScoutSkills.js
Normal file
420
DEBUG_TOTAL_RECOVERY/ZombieScoutSkills.js
Normal file
@@ -0,0 +1,420 @@
|
||||
/**
|
||||
* ZOMBIE SCOUT SKILLS SYSTEM
|
||||
* Skill tree with active/passive abilities
|
||||
* Integrates with ZombieScoutLevelingSystem
|
||||
*/
|
||||
|
||||
export class ZombieScoutSkills {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Skill trees
|
||||
this.skills = new Map();
|
||||
this.unlockedSkills = new Set();
|
||||
this.activeSkills = new Map(); // Currently equipped active skills
|
||||
|
||||
// Skill categories
|
||||
this.categories = ['digging', 'combat', 'utility'];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initializeSkillTree();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize all available skills
|
||||
*/
|
||||
initializeSkillTree() {
|
||||
// DIGGING SKILLS
|
||||
this.registerSkill({
|
||||
id: 'speed_dig',
|
||||
name: 'Speed Dig',
|
||||
category: 'digging',
|
||||
type: 'active',
|
||||
unlockLevel: 10,
|
||||
cooldown: 30000, // 30 seconds
|
||||
duration: 10000, // 10 seconds
|
||||
description: 'Dig 50% faster for 10 seconds',
|
||||
effect: () => this.activateSpeedDig()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'deep_scan',
|
||||
name: 'Deep Scan',
|
||||
category: 'digging',
|
||||
type: 'passive',
|
||||
unlockLevel: 5,
|
||||
description: 'Reveal buried items within 5 tiles',
|
||||
effect: () => this.enableDeepScan()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'treasure_sense',
|
||||
name: 'Treasure Sense',
|
||||
category: 'digging',
|
||||
type: 'active',
|
||||
unlockLevel: 15,
|
||||
cooldown: 60000, // 60 seconds
|
||||
description: 'Reveal all buried treasure on screen',
|
||||
effect: () => this.activateTreasureSense()
|
||||
});
|
||||
|
||||
// COMBAT SKILLS
|
||||
this.registerSkill({
|
||||
id: 'basic_attack',
|
||||
name: 'Claw Swipe',
|
||||
category: 'combat',
|
||||
type: 'active',
|
||||
unlockLevel: 5,
|
||||
cooldown: 2000, // 2 seconds
|
||||
damage: 10,
|
||||
description: 'Basic melee attack dealing 10 damage',
|
||||
effect: () => this.performBasicAttack()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'leap_attack',
|
||||
name: 'Leap Attack',
|
||||
category: 'combat',
|
||||
type: 'active',
|
||||
unlockLevel: 12,
|
||||
cooldown: 8000, // 8 seconds
|
||||
damage: 25,
|
||||
description: 'Leap at enemy dealing 25 damage',
|
||||
effect: () => this.performLeapAttack()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'tank_stance',
|
||||
name: 'Undead Resilience',
|
||||
category: 'combat',
|
||||
type: 'passive',
|
||||
unlockLevel: 8,
|
||||
description: '+20% damage resistance',
|
||||
effect: () => this.enableTankStance()
|
||||
});
|
||||
|
||||
// UTILITY SKILLS
|
||||
this.registerSkill({
|
||||
id: 'scout_speed',
|
||||
name: 'Scout Sprint',
|
||||
category: 'utility',
|
||||
type: 'active',
|
||||
unlockLevel: 7,
|
||||
cooldown: 20000, // 20 seconds
|
||||
duration: 5000, // 5 seconds
|
||||
description: 'Move 100% faster for 5 seconds',
|
||||
effect: () => this.activateScoutSpeed()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'pack_mule',
|
||||
name: 'Pack Mule',
|
||||
category: 'utility',
|
||||
type: 'passive',
|
||||
unlockLevel: 6,
|
||||
description: '+10 inventory slots',
|
||||
effect: () => this.enablePackMule()
|
||||
});
|
||||
|
||||
this.registerSkill({
|
||||
id: 'night_vision',
|
||||
name: 'Night Vision',
|
||||
category: 'utility',
|
||||
type: 'passive',
|
||||
unlockLevel: 10,
|
||||
description: 'See in darkness without penalty',
|
||||
effect: () => this.enableNightVision()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a skill in the system
|
||||
*/
|
||||
registerSkill(skillData) {
|
||||
this.skills.set(skillData.id, {
|
||||
...skillData,
|
||||
unlocked: false,
|
||||
active: false,
|
||||
lastUsed: 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock skill (called when level requirement met)
|
||||
*/
|
||||
unlockSkill(skillId) {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (!skill) return false;
|
||||
|
||||
const scoutLevel = this.scene.zombieScoutLeveling?.currentLevel || 1;
|
||||
|
||||
// Check level requirement
|
||||
if (scoutLevel < skill.unlockLevel) {
|
||||
console.warn(`Scout level ${scoutLevel} too low for ${skill.name} (requires ${skill.unlockLevel})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
skill.unlocked = true;
|
||||
this.unlockedSkills.add(skillId);
|
||||
|
||||
// Auto-activate passive skills
|
||||
if (skill.type === 'passive') {
|
||||
skill.effect();
|
||||
}
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification(
|
||||
`Skill Unlocked: ${skill.name}!`,
|
||||
'skill_unlock',
|
||||
{ description: skill.description }
|
||||
);
|
||||
|
||||
console.log(`✨ Unlocked skill: ${skill.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use active skill
|
||||
*/
|
||||
useSkill(skillId) {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (!skill || !skill.unlocked || skill.type !== 'active') return false;
|
||||
|
||||
// Check cooldown
|
||||
const now = Date.now();
|
||||
const timeSinceLastUse = now - skill.lastUsed;
|
||||
|
||||
if (timeSinceLastUse < skill.cooldown) {
|
||||
const remainingCooldown = Math.ceil((skill.cooldown - timeSinceLastUse) / 1000);
|
||||
console.log(`Skill on cooldown: ${remainingCooldown}s remaining`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Activate skill
|
||||
skill.lastUsed = now;
|
||||
skill.effect();
|
||||
|
||||
// UI feedback
|
||||
this.scene.uiSystem?.showSkillActivation(skill.name, skill.cooldown);
|
||||
|
||||
console.log(`🔥 Activated: ${skill.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* SKILL EFFECTS
|
||||
*/
|
||||
|
||||
activateSpeedDig() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (!scout) return;
|
||||
|
||||
scout.digSpeed *= 1.5; // 50% faster
|
||||
|
||||
// VFX
|
||||
this.scene.vfxSystem?.playEffect('speed_buff', scout.x, scout.y);
|
||||
|
||||
// Restore after duration
|
||||
this.scene.time.delayedCall(10000, () => {
|
||||
scout.digSpeed /= 1.5;
|
||||
console.log('Speed Dig expired');
|
||||
});
|
||||
}
|
||||
|
||||
enableDeepScan() {
|
||||
// Enable passive treasure detection
|
||||
this.scene.gameState.buffs.treasure_detection_range = 5;
|
||||
console.log('Deep Scan enabled: 5 tile range');
|
||||
}
|
||||
|
||||
activateTreasureSense() {
|
||||
// Reveal all buried items on screen
|
||||
this.scene.treasureSystem?.revealAllTreasures();
|
||||
|
||||
// VFX: Screen pulse
|
||||
this.scene.cameras.main.flash(500, 255, 215, 0, true);
|
||||
|
||||
console.log('Treasure Sense activated: All treasures revealed');
|
||||
}
|
||||
|
||||
performBasicAttack() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (!scout) return;
|
||||
|
||||
// Find nearest enemy
|
||||
const enemy = this.findNearestEnemy(scout, 50);
|
||||
if (!enemy) {
|
||||
console.log('No enemy in range');
|
||||
return;
|
||||
}
|
||||
|
||||
// Deal damage
|
||||
enemy.takeDamage(10);
|
||||
|
||||
// VFX
|
||||
this.scene.vfxSystem?.playEffect('claw_swipe', enemy.x, enemy.y);
|
||||
this.scene.soundSystem?.play('zombie_attack');
|
||||
|
||||
console.log('Basic attack: 10 damage');
|
||||
}
|
||||
|
||||
performLeapAttack() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (!scout) return;
|
||||
|
||||
// Find nearest enemy
|
||||
const enemy = this.findNearestEnemy(scout, 150);
|
||||
if (!enemy) return;
|
||||
|
||||
// Leap animation
|
||||
this.scene.tweens.add({
|
||||
targets: scout,
|
||||
x: enemy.x,
|
||||
y: enemy.y - 20,
|
||||
duration: 300,
|
||||
ease: 'Quad.easeOut',
|
||||
onComplete: () => {
|
||||
// Deal damage on landing
|
||||
enemy.takeDamage(25);
|
||||
this.scene.vfxSystem?.playEffect('impact', enemy.x, enemy.y);
|
||||
this.scene.cameras.main.shake(200, 0.008);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Leap attack: 25 damage');
|
||||
}
|
||||
|
||||
enableTankStance() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (scout) {
|
||||
scout.damageResistance = (scout.damageResistance || 0) + 0.2; // +20%
|
||||
console.log('Undead Resilience enabled: +20% damage resistance');
|
||||
}
|
||||
}
|
||||
|
||||
activateScoutSpeed() {
|
||||
const scout = this.scene.zombieScout;
|
||||
if (!scout) return;
|
||||
|
||||
scout.moveSpeed *= 2; // 100% faster
|
||||
|
||||
// VFX: Speed trails
|
||||
this.scene.vfxSystem?.playEffect('speed_trail', scout.x, scout.y, { duration: 5000 });
|
||||
|
||||
// Restore after duration
|
||||
this.scene.time.delayedCall(5000, () => {
|
||||
scout.moveSpeed /= 2;
|
||||
console.log('Scout Sprint expired');
|
||||
});
|
||||
}
|
||||
|
||||
enablePackMule() {
|
||||
this.scene.inventorySystem?.addInventorySlots(10);
|
||||
console.log('Pack Mule enabled: +10 inventory slots');
|
||||
}
|
||||
|
||||
enableNightVision() {
|
||||
this.scene.gameState.buffs.night_vision = true;
|
||||
console.log('Night Vision enabled: Full visibility at night');
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: Find nearest enemy
|
||||
*/
|
||||
findNearestEnemy(from, maxDistance) {
|
||||
let nearest = null;
|
||||
let minDist = maxDistance;
|
||||
|
||||
// Search for enemies in scene
|
||||
const enemies = this.scene.enemies || [];
|
||||
|
||||
enemies.forEach(enemy => {
|
||||
const dist = Phaser.Math.Distance.Between(from.x, from.y, enemy.x, enemy.y);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
nearest = enemy;
|
||||
}
|
||||
});
|
||||
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unlocked skills by category
|
||||
*/
|
||||
getSkillsByCategory(category) {
|
||||
return Array.from(this.skills.values())
|
||||
.filter(s => s.category === category && s.unlocked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unlocked skills
|
||||
*/
|
||||
getUnlockedSkills() {
|
||||
return Array.from(this.skills.values()).filter(s => s.unlocked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check skill availability
|
||||
*/
|
||||
isSkillAvailable(skillId) {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (!skill || !skill.unlocked) return false;
|
||||
|
||||
if (skill.type === 'passive') return true;
|
||||
|
||||
const now = Date.now();
|
||||
return (now - skill.lastUsed) >= skill.cooldown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get skill cooldown remaining
|
||||
*/
|
||||
getCooldownRemaining(skillId) {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (!skill) return 0;
|
||||
|
||||
const now = Date.now();
|
||||
const elapsed = now - skill.lastUsed;
|
||||
const remaining = Math.max(0, skill.cooldown - elapsed);
|
||||
|
||||
return Math.ceil(remaining / 1000); // Return in seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Save/Load
|
||||
*/
|
||||
getSaveData() {
|
||||
return {
|
||||
unlockedSkills: Array.from(this.unlockedSkills),
|
||||
skillCooldowns: Array.from(this.skills.entries()).map(([id, skill]) => ({
|
||||
id,
|
||||
lastUsed: skill.lastUsed
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
loadSaveData(data) {
|
||||
this.unlockedSkills = new Set(data.unlockedSkills || []);
|
||||
|
||||
// Restore cooldowns
|
||||
data.skillCooldowns?.forEach(({ id, lastUsed }) => {
|
||||
const skill = this.skills.get(id);
|
||||
if (skill) {
|
||||
skill.lastUsed = lastUsed;
|
||||
skill.unlocked = this.unlockedSkills.has(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Re-apply passive skills
|
||||
this.unlockedSkills.forEach(skillId => {
|
||||
const skill = this.skills.get(skillId);
|
||||
if (skill && skill.type === 'passive') {
|
||||
skill.effect();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
875
DEBUG_TOTAL_RECOVERY/ZombieSystem.js
Normal file
875
DEBUG_TOTAL_RECOVERY/ZombieSystem.js
Normal file
@@ -0,0 +1,875 @@
|
||||
/**
|
||||
* ZombieSystem.js
|
||||
* ===============
|
||||
* KRVAVA ŽETEV - Zombie Worker Management System
|
||||
*
|
||||
* Core Concept:
|
||||
* Player is "Alfa" (hybrid virus) - can tame wild zombies
|
||||
* Zombies can be assigned tasks: farming, mining, gathering, guarding
|
||||
* Zombies level up, specialize, get tired, and eventually decay
|
||||
*
|
||||
* Features:
|
||||
* - Zombie taming (Alfa scent system)
|
||||
* - AI pathfinding & task execution
|
||||
* - Skill specialization (Farmer, Miner, Guard, Gatherer)
|
||||
* - Stamina & decay mechanics
|
||||
* - Grave resting system (regeneration)
|
||||
* - Work commands (follow, farm, mine, gather, guard)
|
||||
*
|
||||
* Uses: zombie_varieties_pack_tiled_1766101086057.tsx
|
||||
* zombie_workers_2x2_grids_1766099189858.tsx
|
||||
* smart_zombies_working_1766097073226.tsx
|
||||
* specialty_zombie_workers_detailed_1766097635926.tsx
|
||||
*
|
||||
* @author NovaFarma Team
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
|
||||
export default class ZombieSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
|
||||
// Zombie registry
|
||||
this.zombies = new Map(); // id -> zombie data
|
||||
this.wildZombies = new Set(); // Untamed zombies
|
||||
this.tamedZombies = new Set(); // Player's zombies
|
||||
|
||||
// Alfa system
|
||||
this.alfaScent = 100; // Player's Alfa power (0-100)
|
||||
this.alfaRange = 150; // Taming range (pixels)
|
||||
|
||||
// Task queues
|
||||
this.taskQueue = new Map(); // zombieId -> task
|
||||
|
||||
// Graves
|
||||
this.graves = new Map(); // position -> grave data
|
||||
|
||||
// Zombie definitions
|
||||
this.zombieTypes = this.defineZombieTypes();
|
||||
this.skillTrees = this.defineSkillTrees();
|
||||
|
||||
// Timing constants
|
||||
this.DECAY_RATE = 0.1; // HP loss per second when not resting
|
||||
this.STAMINA_DRAIN_RATE = 5; // Stamina loss per second when working
|
||||
this.STAMINA_REGEN_RATE = 20; // Stamina gain per second when resting
|
||||
this.GRAVE_REST_BONUS = 2.0; // 2x regen in grave
|
||||
|
||||
console.log('🧟 ZombieSystem initialized - ALFA ACTIVE');
|
||||
}
|
||||
|
||||
defineZombieTypes() {
|
||||
return {
|
||||
basic: {
|
||||
name: 'Basic Zombie',
|
||||
baseHP: 100,
|
||||
baseStamina: 100,
|
||||
baseSpeed: 60,
|
||||
baseWorkSpeed: 1.0,
|
||||
sprite: 'zombie_varieties_pack', // Frame 0
|
||||
tameDifficulty: 1
|
||||
},
|
||||
worker: {
|
||||
name: 'Zombie Worker',
|
||||
baseHP: 120,
|
||||
baseStamina: 150,
|
||||
baseSpeed: 70,
|
||||
baseWorkSpeed: 1.2,
|
||||
sprite: 'zombie_workers_2x2_grids', // Frame 0
|
||||
tameDifficulty: 2
|
||||
},
|
||||
smart: {
|
||||
name: 'Smart Zombie',
|
||||
baseHP: 150,
|
||||
baseStamina: 120,
|
||||
baseSpeed: 80,
|
||||
baseWorkSpeed: 1.5,
|
||||
sprite: 'smart_zombies_working',
|
||||
tameDifficulty: 3,
|
||||
canLearn: true // Levels faster
|
||||
},
|
||||
elite: {
|
||||
name: 'Elite Zombie',
|
||||
baseHP: 200,
|
||||
baseStamina: 200,
|
||||
baseSpeed: 100,
|
||||
baseWorkSpeed: 2.0,
|
||||
sprite: 'elite_zombie',
|
||||
tameDifficulty: 5,
|
||||
canLearn: true,
|
||||
specialAbility: 'multitask' // Can do 2 tasks
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
defineSkillTrees() {
|
||||
return {
|
||||
farmer: {
|
||||
name: 'Farmer Zombie',
|
||||
tasks: ['plant', 'harvest', 'water', 'fertilize'],
|
||||
levelBonuses: {
|
||||
1: { workSpeed: 1.0 },
|
||||
5: { workSpeed: 1.2, yieldBonus: 1.1 },
|
||||
10: { workSpeed: 1.5, yieldBonus: 1.3, autoPlant: true },
|
||||
15: { workSpeed: 2.0, yieldBonus: 1.5, qualityBonus: 1.2 }
|
||||
},
|
||||
sprite: 'specialty_zombie_workers', // Frame 0
|
||||
color: 0x00FF00 // Green tint
|
||||
},
|
||||
miner: {
|
||||
name: 'Miner Zombie',
|
||||
tasks: ['mine', 'quarry', 'smelt'],
|
||||
levelBonuses: {
|
||||
1: { workSpeed: 1.0 },
|
||||
5: { workSpeed: 1.2, oreBonus: 1.1 },
|
||||
10: { workSpeed: 1.5, oreBonus: 1.3, rareOreChance: 0.1 },
|
||||
15: { workSpeed: 2.0, oreBonus: 1.5, gemChance: 0.2 }
|
||||
},
|
||||
sprite: 'specialty_zombie_workers', // Frame 1
|
||||
color: 0x888888 // Gray tint
|
||||
},
|
||||
gatherer: {
|
||||
name: 'Gatherer Zombie',
|
||||
tasks: ['gather', 'forage', 'loot'],
|
||||
levelBonuses: {
|
||||
1: { workSpeed: 1.0 },
|
||||
5: { workSpeed: 1.2, gatherRadius: 1.2 },
|
||||
10: { workSpeed: 1.5, gatherRadius: 1.5, rareItemChance: 0.1 },
|
||||
15: { workSpeed: 2.0, gatherRadius: 2.0, doubleGather: 0.3 }
|
||||
},
|
||||
sprite: 'specialty_zombie_workers', // Frame 2
|
||||
color: 0xFFFF00 // Yellow tint
|
||||
},
|
||||
guard: {
|
||||
name: 'Guard Zombie',
|
||||
tasks: ['guard', 'patrol', 'attack'],
|
||||
levelBonuses: {
|
||||
1: { damage: 10, defense: 5 },
|
||||
5: { damage: 20, defense: 10, detectRange: 1.2 },
|
||||
10: { damage: 35, defense: 20, detectRange: 1.5, counterAttack: true },
|
||||
15: { damage: 50, defense: 30, detectRange: 2.0, areaAttack: true }
|
||||
},
|
||||
sprite: 'specialty_zombie_workers', // Frame 3
|
||||
color: 0xFF0000 // Red tint
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ===== ZOMBIE SPAWNING =====
|
||||
|
||||
spawnWildZombie(x, y, type = 'basic') {
|
||||
const zombieType = this.zombieTypes[type];
|
||||
if (!zombieType) return null;
|
||||
|
||||
const zombie = {
|
||||
id: this.generateId(),
|
||||
type: type,
|
||||
state: 'wild', // wild, tamed, working, resting, decaying
|
||||
|
||||
// Position
|
||||
x: x,
|
||||
y: y,
|
||||
|
||||
// Stats
|
||||
hp: zombieType.baseHP,
|
||||
maxHP: zombieType.baseHP,
|
||||
stamina: zombieType.baseStamina,
|
||||
maxStamina: zombieType.baseStamina,
|
||||
|
||||
// Progression
|
||||
level: 1,
|
||||
xp: 0,
|
||||
xpToNextLevel: 100,
|
||||
specialization: null, // farmer, miner, gatherer, guard
|
||||
|
||||
// Work
|
||||
currentTask: null,
|
||||
workSpeed: zombieType.baseWorkSpeed,
|
||||
efficiency: 1.0,
|
||||
|
||||
// Decay
|
||||
decayTimer: 24 * 60 * 60 * 1000, // 24 hours to decay
|
||||
decayRate: this.DECAY_RATE,
|
||||
|
||||
// Behavior
|
||||
speed: zombieType.baseSpeed,
|
||||
aggression: 0.3, // Wild zombies slightly aggressive
|
||||
loyalty: 0, // Increases when tamed
|
||||
|
||||
// Visual
|
||||
sprite: null,
|
||||
tint: 0xFFFFFF,
|
||||
|
||||
// Timestamps
|
||||
spawnTime: Date.now(),
|
||||
lastWorked: null,
|
||||
lastRested: null
|
||||
};
|
||||
|
||||
// Create sprite
|
||||
zombie.sprite = this.createZombieSprite(zombie, zombieType);
|
||||
|
||||
// Add to registry
|
||||
this.zombies.set(zombie.id, zombie);
|
||||
this.wildZombies.add(zombie.id);
|
||||
|
||||
console.log(`🧟 Spawned wild ${zombieType.name} at (${x}, ${y})`);
|
||||
return zombie;
|
||||
}
|
||||
|
||||
createZombieSprite(zombie, zombieType) {
|
||||
const sprite = this.scene.add.sprite(zombie.x, zombie.y, zombieType.sprite);
|
||||
|
||||
sprite.setData('zombieId', zombie.id);
|
||||
sprite.setInteractive();
|
||||
|
||||
// Physics
|
||||
this.scene.physics.add.existing(sprite);
|
||||
sprite.body.setCollideWorldBounds(true);
|
||||
|
||||
// Click handler
|
||||
sprite.on('pointerdown', () => {
|
||||
this.onZombieClicked(zombie);
|
||||
});
|
||||
|
||||
// Health bar
|
||||
this.createHealthBar(sprite);
|
||||
|
||||
return sprite;
|
||||
}
|
||||
|
||||
createHealthBar(sprite) {
|
||||
const bar = this.scene.add.graphics();
|
||||
bar.fillStyle(0xFF0000);
|
||||
bar.fillRect(-20, -30, 40, 4);
|
||||
sprite.healthBar = bar;
|
||||
sprite.healthBar.x = sprite.x;
|
||||
sprite.healthBar.y = sprite.y;
|
||||
}
|
||||
|
||||
// ===== ALFA SYSTEM (TAMING) =====
|
||||
|
||||
canTameZombie(zombieId) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) {
|
||||
return { canTame: false, reason: 'Zombie not found' };
|
||||
}
|
||||
|
||||
if (zombie.state !== 'wild') {
|
||||
return { canTame: false, reason: 'Already tamed' };
|
||||
}
|
||||
|
||||
// Check distance (Alfa scent range)
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
this.scene.player.x, this.scene.player.y,
|
||||
zombie.x, zombie.y
|
||||
);
|
||||
|
||||
if (distance > this.alfaRange) {
|
||||
return { canTame: false, reason: 'Too far - get closer!' };
|
||||
}
|
||||
|
||||
// Check Alfa power
|
||||
const zombieType = this.zombieTypes[zombie.type];
|
||||
const requiredAlfaPower = zombieType.tameDifficulty * 15;
|
||||
|
||||
if (this.alfaScent < requiredAlfaPower) {
|
||||
return {
|
||||
canTame: false,
|
||||
reason: `Need ${requiredAlfaPower} Alfa power (have ${this.alfaScent})`
|
||||
};
|
||||
}
|
||||
|
||||
return { canTame: true };
|
||||
}
|
||||
|
||||
tameZombie(zombieId) {
|
||||
const check = this.canTameZombie(zombieId);
|
||||
if (!check.canTame) {
|
||||
this.scene.uiSystem?.showError(check.reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
const zombieType = this.zombieTypes[zombie.type];
|
||||
|
||||
// Taming animation
|
||||
if (this.scene.particleSystem) {
|
||||
this.scene.particleSystem.createTamingEffect(zombie.x, zombie.y);
|
||||
}
|
||||
|
||||
// Update zombie state
|
||||
zombie.state = 'tamed';
|
||||
zombie.loyalty = 50; // Starts at 50, increases with work
|
||||
zombie.aggression = 0;
|
||||
|
||||
// Remove from wild, add to tamed
|
||||
this.wildZombies.delete(zombieId);
|
||||
this.tamedZombies.add(zombieId);
|
||||
|
||||
// Visual feedback
|
||||
zombie.sprite.setTint(0x00FF88); // Green tint for tamed
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification({
|
||||
title: '🧟 Zombie Tamed!',
|
||||
message: `${zombieType.name} joined your workforce!`,
|
||||
icon: 'zombie',
|
||||
duration: 4000,
|
||||
color: '#00FF00'
|
||||
});
|
||||
|
||||
// Emit event
|
||||
this.scene.events.emit('zombieTamed', { zombie });
|
||||
|
||||
// Consume Alfa power
|
||||
const cost = zombieType.tameDifficulty * 10;
|
||||
this.alfaScent = Math.max(0, this.alfaScent - cost);
|
||||
|
||||
console.log(`🧟 Tamed ${zombieType.name} - Loyalty: ${zombie.loyalty}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== TASK ASSIGNMENT =====
|
||||
|
||||
assignTask(zombieId, task) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie || zombie.state !== 'tamed') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if zombie has specialization for this task
|
||||
if (zombie.specialization) {
|
||||
const spec = this.skillTrees[zombie.specialization];
|
||||
if (!spec.tasks.includes(task.type)) {
|
||||
this.scene.uiSystem?.showError(`This zombie specializes in ${zombie.specialization} tasks!`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign task
|
||||
zombie.currentTask = task;
|
||||
zombie.state = 'working';
|
||||
this.taskQueue.set(zombieId, task);
|
||||
|
||||
console.log(`🧟 ${zombie.id} assigned task:`, task.type);
|
||||
return true;
|
||||
}
|
||||
|
||||
executeTask(zombie, delta) {
|
||||
const task = zombie.currentTask;
|
||||
if (!task) return;
|
||||
|
||||
const deltaSeconds = delta / 1000;
|
||||
|
||||
// Drain stamina
|
||||
zombie.stamina -= this.STAMINA_DRAIN_RATE * deltaSeconds;
|
||||
|
||||
// If exhausted, stop working
|
||||
if (zombie.stamina <= 0) {
|
||||
zombie.state = 'idle';
|
||||
zombie.currentTask = null;
|
||||
this.scene.uiSystem?.showNotification({
|
||||
title: 'Zombie Exhausted!',
|
||||
message: `Zombie ${zombie.id} needs rest`,
|
||||
icon: 'zombie',
|
||||
duration: 2000
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute task based on type
|
||||
switch (task.type) {
|
||||
case 'farm':
|
||||
this.executeFarmTask(zombie, task, deltaSeconds);
|
||||
break;
|
||||
case 'mine':
|
||||
this.executeMineTask(zombie, task, deltaSeconds);
|
||||
break;
|
||||
case 'gather':
|
||||
this.executeGatherTask(zombie, task, deltaSeconds);
|
||||
break;
|
||||
case 'guard':
|
||||
this.executeGuardTask(zombie, task, deltaSeconds);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
executeFarmTask(zombie, task, deltaSeconds) {
|
||||
// Move to farm plot
|
||||
this.moveTowardsTarget(zombie, task.targetX, task.targetY);
|
||||
|
||||
// If close enough, work
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
zombie.x, zombie.y, task.targetX, task.targetY
|
||||
);
|
||||
|
||||
if (distance < 20) {
|
||||
task.progress = (task.progress || 0) + (zombie.workSpeed * deltaSeconds);
|
||||
|
||||
// Complete task
|
||||
if (task.progress >= task.requiredProgress) {
|
||||
this.completeTask(zombie, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executeMineTask(zombie, task, deltaSeconds) {
|
||||
this.moveTowardsTarget(zombie, task.targetX, task.targetY);
|
||||
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
zombie.x, zombie.y, task.targetX, task.targetY
|
||||
);
|
||||
|
||||
if (distance < 20) {
|
||||
task.progress = (task.progress || 0) + (zombie.workSpeed * 0.8 * deltaSeconds);
|
||||
|
||||
if (task.progress >= task.requiredProgress) {
|
||||
this.completeTask(zombie, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executeGatherTask(zombie, task, deltaSeconds) {
|
||||
this.moveTowardsTarget(zombie, task.targetX, task.targetY);
|
||||
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
zombie.x, zombie.y, task.targetX, task.targetY
|
||||
);
|
||||
|
||||
if (distance < 30) {
|
||||
task.progress = (task.progress || 0) + (zombie.workSpeed * 1.2 * deltaSeconds);
|
||||
|
||||
if (task.progress >= task.requiredProgress) {
|
||||
this.completeTask(zombie, task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executeGuardTask(zombie, task, deltaSeconds) {
|
||||
// Patrol area
|
||||
if (!task.patrolTarget || zombie.reachedPatrol) {
|
||||
task.patrolTarget = {
|
||||
x: task.centerX + Phaser.Math.Between(-task.radius, task.radius),
|
||||
y: task.centerY + Phaser.Math.Between(-task.radius, task.radius)
|
||||
};
|
||||
zombie.reachedPatrol = false;
|
||||
}
|
||||
|
||||
this.moveTowardsTarget(zombie, task.patrolTarget.x, task.patrolTarget.y);
|
||||
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
zombie.x, zombie.y, task.patrolTarget.x, task.patrolTarget.y
|
||||
);
|
||||
|
||||
if (distance < 10) {
|
||||
zombie.reachedPatrol = true;
|
||||
}
|
||||
|
||||
// Check for enemies
|
||||
this.detectEnemies(zombie, task.radius);
|
||||
}
|
||||
|
||||
completeTask(zombie, task) {
|
||||
// Award XP
|
||||
const xpGain = task.xpReward || 20;
|
||||
this.addXP(zombie, xpGain);
|
||||
|
||||
// Increase loyalty
|
||||
zombie.loyalty = Math.min(100, zombie.loyalty + 1);
|
||||
|
||||
// Clear task
|
||||
zombie.currentTask = null;
|
||||
zombie.state = 'idle';
|
||||
this.taskQueue.delete(zombie.id);
|
||||
|
||||
// Emit event
|
||||
this.scene.events.emit('zombieTaskComplete', { zombie, task });
|
||||
|
||||
console.log(`✅ Zombie ${zombie.id} completed ${task.type} task (+${xpGain} XP)`);
|
||||
}
|
||||
|
||||
// ===== LEVELING & SPECIALIZATION =====
|
||||
|
||||
addXP(zombie, amount) {
|
||||
zombie.xp += amount;
|
||||
|
||||
// Check for level up
|
||||
while (zombie.xp >= zombie.xpToNextLevel) {
|
||||
this.levelUp(zombie);
|
||||
}
|
||||
}
|
||||
|
||||
levelUp(zombie) {
|
||||
zombie.level++;
|
||||
zombie.xp -= zombie.xpToNextLevel;
|
||||
zombie.xpToNextLevel = Math.floor(zombie.xpToNextLevel * 1.5);
|
||||
|
||||
// Increase stats
|
||||
zombie.maxHP += 10;
|
||||
zombie.hp = zombie.maxHP;
|
||||
zombie.maxStamina += 10;
|
||||
zombie.stamina = zombie.maxStamina;
|
||||
zombie.workSpeed += 0.1;
|
||||
|
||||
// Apply specialization bonuses
|
||||
if (zombie.specialization) {
|
||||
const spec = this.skillTrees[zombie.specialization];
|
||||
const levelBonuses = spec.levelBonuses[zombie.level];
|
||||
|
||||
if (levelBonuses) {
|
||||
Object.assign(zombie, levelBonuses);
|
||||
}
|
||||
}
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification({
|
||||
title: '🧟 LEVEL UP!',
|
||||
message: `Zombie ${zombie.id} reached level ${zombie.level}!`,
|
||||
icon: 'zombie',
|
||||
duration: 3000,
|
||||
color: '#FFD700'
|
||||
});
|
||||
|
||||
console.log(`🆙 Zombie ${zombie.id} leveled up to ${zombie.level}!`);
|
||||
}
|
||||
|
||||
specializeZombie(zombieId, specialization) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return false;
|
||||
|
||||
if (zombie.specialization) {
|
||||
this.scene.uiSystem?.showError('Already specialized!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (zombie.level < 3) {
|
||||
this.scene.uiSystem?.showError('Zombie must be level 3+ to specialize');
|
||||
return false;
|
||||
}
|
||||
|
||||
const spec = this.skillTrees[specialization];
|
||||
zombie.specialization = specialization;
|
||||
zombie.sprite.setTint(spec.color);
|
||||
|
||||
this.scene.uiSystem?.showNotification({
|
||||
title: '⚡ Specialization Unlocked!',
|
||||
message: `Zombie is now a ${spec.name}!`,
|
||||
icon: 'zombie',
|
||||
duration: 4000,
|
||||
color: '#00FFFF'
|
||||
});
|
||||
|
||||
console.log(`⚡ Zombie ${zombie.id} specialized as ${spec.name}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== DECAY SYSTEM =====
|
||||
|
||||
updateDecay(zombie, delta) {
|
||||
if (zombie.state === 'resting') return; // No decay when resting
|
||||
|
||||
const deltaSeconds = delta / 1000;
|
||||
|
||||
// Drain decay timer
|
||||
zombie.decayTimer -= deltaSeconds * 1000;
|
||||
|
||||
// If timer expired, start losing HP
|
||||
if (zombie.decayTimer <= 0) {
|
||||
zombie.hp -= zombie.decayRate * deltaSeconds;
|
||||
|
||||
// Death check
|
||||
if (zombie.hp <= 0) {
|
||||
this.zombieDeath(zombie);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zombieDeath(zombie) {
|
||||
console.log(`💀 Zombie ${zombie.id} has decayed completely`);
|
||||
|
||||
// Drop fertilizer (good for farming!)
|
||||
if (this.scene.farmingSystem) {
|
||||
this.scene.farmingSystem.spawnFertilizer(zombie.x, zombie.y, 5);
|
||||
}
|
||||
|
||||
// Award player XP
|
||||
if (this.scene.player) {
|
||||
this.scene.player.addXP?.(zombie.level * 10);
|
||||
}
|
||||
|
||||
// Remove zombie
|
||||
zombie.sprite?.destroy();
|
||||
zombie.healthBar?.destroy();
|
||||
this.zombies.delete(zombie.id);
|
||||
this.tamedZombies.delete(zombie.id);
|
||||
|
||||
// Notification
|
||||
this.scene.uiSystem?.showNotification({
|
||||
title: '💀 Zombie Decayed',
|
||||
message: `Zombie turned to fertilizer (+${zombie.level * 10} XP)`,
|
||||
icon: 'zombie',
|
||||
duration: 3000
|
||||
});
|
||||
}
|
||||
|
||||
// ===== GRAVE SYSTEM =====
|
||||
|
||||
createGrave(x, y) {
|
||||
const graveId = `${x}_${y}`;
|
||||
|
||||
if (this.graves.has(graveId)) {
|
||||
this.scene.uiSystem?.showError('Grave already exists here!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if player has materials
|
||||
if (this.scene.recipeSystem) {
|
||||
const canCraft = this.scene.recipeSystem.canCraft('grave');
|
||||
if (!canCraft.canCraft) {
|
||||
this.scene.uiSystem?.showError(canCraft.reason);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const grave = {
|
||||
id: graveId,
|
||||
x: x,
|
||||
y: y,
|
||||
occupied: false,
|
||||
zombieId: null,
|
||||
sprite: this.scene.add.sprite(x, y, 'gravestone')
|
||||
};
|
||||
|
||||
this.graves.set(graveId, grave);
|
||||
|
||||
console.log(`⚰️ Created grave at (${x}, ${y})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
zombieRest(zombieId, graveId) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
const grave = this.graves.get(graveId);
|
||||
|
||||
if (!zombie || !grave) return false;
|
||||
|
||||
if (grave.occupied) {
|
||||
this.scene.uiSystem?.showError('Grave is occupied!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Move zombie to grave
|
||||
zombie.state = 'resting';
|
||||
zombie.sprite.setPosition(grave.x, grave.y);
|
||||
zombie.sprite.setAlpha(0.5); // Semi-transparent
|
||||
|
||||
grave.occupied = true;
|
||||
grave.zombieId = zombieId;
|
||||
|
||||
console.log(`😴 Zombie ${zombieId} resting in grave`);
|
||||
return true;
|
||||
}
|
||||
|
||||
updateResting(zombie, delta) {
|
||||
const deltaSeconds = delta / 1000;
|
||||
|
||||
// Regenerate stamina (2x faster in grave)
|
||||
zombie.stamina = Math.min(
|
||||
zombie.maxStamina,
|
||||
zombie.stamina + this.STAMINA_REGEN_RATE * this.GRAVE_REST_BONUS * deltaSeconds
|
||||
);
|
||||
|
||||
// Slow HP regen
|
||||
zombie.hp = Math.min(
|
||||
zombie.maxHP,
|
||||
zombie.hp + 2 * deltaSeconds
|
||||
);
|
||||
|
||||
// Restore decay timer
|
||||
zombie.decayTimer += 1000 * deltaSeconds; // Add 1 second per second resting
|
||||
|
||||
// If fully rested, wake up
|
||||
if (zombie.stamina >= zombie.maxStamina) {
|
||||
this.wakeUpZombie(zombie);
|
||||
}
|
||||
}
|
||||
|
||||
wakeUpZombie(zombie) {
|
||||
zombie.state = 'idle';
|
||||
zombie.sprite.setAlpha(1.0);
|
||||
|
||||
// Find and free grave
|
||||
for (const [graveId, grave] of this.graves.entries()) {
|
||||
if (grave.zombieId === zombie.id) {
|
||||
grave.occupied = false;
|
||||
grave.zombieId = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`😊 Zombie ${zombie.id} woke up refreshed!`);
|
||||
}
|
||||
|
||||
// ===== MOVEMENT & PATHFINDING =====
|
||||
|
||||
moveTowardsTarget(zombie, targetX, targetY) {
|
||||
const angle = Phaser.Math.Angle.Between(zombie.x, zombie.y, targetX, targetY);
|
||||
|
||||
const velocityX = Math.cos(angle) * zombie.speed;
|
||||
const velocityY = Math.sin(angle) * zombie.speed;
|
||||
|
||||
zombie.sprite.body.setVelocity(velocityX, velocityY);
|
||||
|
||||
// Update position
|
||||
zombie.x = zombie.sprite.x;
|
||||
zombie.y = zombie.sprite.y;
|
||||
}
|
||||
|
||||
detectEnemies(zombie, radius) {
|
||||
// Guard zombies detect and attack enemies
|
||||
if (!this.scene.enemies) return;
|
||||
|
||||
this.scene.enemies.getChildren().forEach(enemy => {
|
||||
const distance = Phaser.Math.Distance.Between(
|
||||
zombie.x, zombie.y, enemy.x, enemy.y
|
||||
);
|
||||
|
||||
if (distance < radius) {
|
||||
this.zombieAttack(zombie, enemy);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
zombieAttack(zombie, enemy) {
|
||||
const spec = zombie.specialization === 'guard' ?
|
||||
this.skillTrees.guard.levelBonuses[zombie.level] : null;
|
||||
|
||||
const damage = spec ? spec.damage : 10;
|
||||
enemy.takeDamage?.(damage);
|
||||
|
||||
// Gain XP for combat
|
||||
this.addXP(zombie, 5);
|
||||
}
|
||||
|
||||
// ===== COMMANDS =====
|
||||
|
||||
commandFollow(zombieId) {
|
||||
const zombie = this.zombies.get(zombieId);
|
||||
if (!zombie) return;
|
||||
|
||||
zombie.currentTask = {
|
||||
type: 'follow',
|
||||
targetX: this.scene.player.x,
|
||||
targetY: this.scene.player.y
|
||||
};
|
||||
zombie.state = 'following';
|
||||
}
|
||||
|
||||
commandGuardArea(zombieId, x, y, radius = 100) {
|
||||
this.assignTask(zombieId, {
|
||||
type: 'guard',
|
||||
centerX: x,
|
||||
centerY: y,
|
||||
radius: radius,
|
||||
requiredProgress: Infinity // Never ends
|
||||
});
|
||||
}
|
||||
|
||||
// ===== HELPER FUNCTIONS =====
|
||||
|
||||
generateId() {
|
||||
return `zombie_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
onZombieClicked(zombie) {
|
||||
this.scene.events.emit('zombieSelected', { zombie });
|
||||
// Show zombie info UI
|
||||
}
|
||||
|
||||
// ===== GETTERS =====
|
||||
|
||||
getTamedZombies() {
|
||||
return Array.from(this.tamedZombies).map(id => this.zombies.get(id));
|
||||
}
|
||||
|
||||
getWorkingZombies() {
|
||||
return this.getTamedZombies().filter(z => z.state === 'working');
|
||||
}
|
||||
|
||||
getIdleZombies() {
|
||||
return this.getTamedZombies().filter(z => z.state === 'idle');
|
||||
}
|
||||
|
||||
getZombiesBySpecialization(spec) {
|
||||
return this.getTamedZombies().filter(z => z.specialization === spec);
|
||||
}
|
||||
|
||||
// ===== UPDATE =====
|
||||
|
||||
update(time, delta) {
|
||||
for (const [id, zombie] of this.zombies.entries()) {
|
||||
// Update based on state
|
||||
switch (zombie.state) {
|
||||
case 'working':
|
||||
this.executeTask(zombie, delta);
|
||||
this.updateDecay(zombie, delta);
|
||||
break;
|
||||
|
||||
case 'resting':
|
||||
this.updateResting(zombie, delta);
|
||||
break;
|
||||
|
||||
case 'following':
|
||||
this.moveTowardsTarget(zombie, this.scene.player.x, this.scene.player.y);
|
||||
this.updateDecay(zombie, delta);
|
||||
break;
|
||||
|
||||
case 'idle':
|
||||
case 'tamed':
|
||||
this.updateDecay(zombie, delta);
|
||||
break;
|
||||
|
||||
case 'wild':
|
||||
// Wild zombies wander
|
||||
if (Math.random() < 0.01) {
|
||||
const wanderX = zombie.x + Phaser.Math.Between(-50, 50);
|
||||
const wanderY = zombie.y + Phaser.Math.Between(-50, 50);
|
||||
this.moveTowardsTarget(zombie, wanderX, wanderY);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Update health bar
|
||||
if (zombie.healthBar && zombie.sprite) {
|
||||
zombie.healthBar.x = zombie.sprite.x;
|
||||
zombie.healthBar.y = zombie.sprite.y;
|
||||
|
||||
const healthPercent = zombie.hp / zombie.maxHP;
|
||||
zombie.healthBar.clear();
|
||||
zombie.healthBar.fillStyle(healthPercent > 0.5 ? 0x00FF00 : 0xFF0000);
|
||||
zombie.healthBar.fillRect(-20, -30, 40 * healthPercent, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Regenerate Alfa scent slowly
|
||||
this.alfaScent = Math.min(100, this.alfaScent + 0.01 * (delta / 1000));
|
||||
}
|
||||
|
||||
// ===== CLEANUP =====
|
||||
|
||||
destroy() {
|
||||
for (const zombie of this.zombies.values()) {
|
||||
zombie.sprite?.destroy();
|
||||
zombie.healthBar?.destroy();
|
||||
}
|
||||
|
||||
for (const grave of this.graves.values()) {
|
||||
grave.sprite?.destroy();
|
||||
}
|
||||
|
||||
this.zombies.clear();
|
||||
this.wildZombies.clear();
|
||||
this.tamedZombies.clear();
|
||||
this.graves.clear();
|
||||
this.taskQueue.clear();
|
||||
}
|
||||
}
|
||||
239
DEBUG_TOTAL_RECOVERY/ZombieWorkerSystem.js
Normal file
239
DEBUG_TOTAL_RECOVERY/ZombieWorkerSystem.js
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* ZOMBIE WORKER AI SYSTEM
|
||||
* Tamed zombies lahko opravljajo delo (farming, mining)
|
||||
*/
|
||||
class ZombieWorkerSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.workers = [];
|
||||
}
|
||||
|
||||
assignWork(zombie, workType, workRadius = 5) {
|
||||
if (!zombie.isTamed) {
|
||||
console.warn('⚠️ Cannot assign work to untamed zombie!');
|
||||
return false;
|
||||
}
|
||||
|
||||
zombie.workerData = {
|
||||
type: workType,
|
||||
radius: workRadius,
|
||||
centerX: zombie.gridX,
|
||||
centerY: zombie.gridY,
|
||||
energy: 100,
|
||||
decayRate: 0.5,
|
||||
workTimer: 0,
|
||||
workInterval: 5000,
|
||||
status: 'IDLE',
|
||||
inventory: {
|
||||
seeds: 0,
|
||||
hoe: 0,
|
||||
watering_can: 0,
|
||||
pickaxe: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.workers.push(zombie);
|
||||
console.log(`🧟 Assigned ${zombie.type} as ${workType} worker`);
|
||||
return true;
|
||||
}
|
||||
|
||||
removeWorker(zombie) {
|
||||
const index = this.workers.indexOf(zombie);
|
||||
if (index > -1) {
|
||||
this.workers.splice(index, 1);
|
||||
zombie.workerData = null;
|
||||
}
|
||||
}
|
||||
|
||||
update(delta) {
|
||||
this.workers = this.workers.filter(w =>
|
||||
this.scene.npcs.includes(w) && w.hp > 0
|
||||
);
|
||||
|
||||
for (const worker of this.workers) {
|
||||
if (!worker.workerData) continue;
|
||||
|
||||
this.applyDecay(worker, delta);
|
||||
worker.workerData.workTimer += delta;
|
||||
|
||||
if (worker.workerData.workTimer >= worker.workerData.workInterval) {
|
||||
worker.workerData.workTimer = 0;
|
||||
|
||||
if (worker.workerData.type === 'FARM') {
|
||||
this.performFarmWork(worker);
|
||||
} else if (worker.workerData.type === 'MINE') {
|
||||
this.performMineWork(worker);
|
||||
} else if (worker.workerData.type === 'CLEAR') {
|
||||
this.performClearWork(worker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
applyDecay(zombie, delta) {
|
||||
const wd = zombie.workerData;
|
||||
wd.energy -= (wd.decayRate * delta) / 1000;
|
||||
|
||||
if (wd.energy <= 0) {
|
||||
wd.energy = 0;
|
||||
zombie.hp -= (wd.decayRate * delta) / 1000;
|
||||
if (zombie.sprite) zombie.sprite.setTint(0x666666);
|
||||
}
|
||||
|
||||
if (zombie.hp <= 0) this.onWorkerDeath(zombie);
|
||||
}
|
||||
|
||||
performFarmWork(zombie) {
|
||||
const wd = zombie.workerData;
|
||||
const terrain = this.scene.terrainSystem;
|
||||
const farming = this.scene.farmingSystem;
|
||||
if (!terrain || !farming) return;
|
||||
|
||||
// 1. Water/Harvest existing crops
|
||||
if (terrain.cropsMap) {
|
||||
for (let dx = -wd.radius; dx <= wd.radius; dx++) {
|
||||
for (let dy = -wd.radius; dy <= wd.radius; dy++) {
|
||||
const key = `${wd.centerX + dx},${wd.centerY + dy}`;
|
||||
if (terrain.cropsMap.has(key)) {
|
||||
const crop = terrain.cropsMap.get(key);
|
||||
|
||||
if (!crop.isWatered) {
|
||||
farming.waterCrop(wd.centerX + dx, wd.centerY + dy);
|
||||
console.log(`🧟💧 Worker watered`);
|
||||
wd.status = 'WORKING';
|
||||
return;
|
||||
}
|
||||
|
||||
if (crop.stage >= 4) {
|
||||
farming.harvest(wd.centerX + dx, wd.centerY + dy);
|
||||
console.log(`🧟🌾 Worker harvested`);
|
||||
wd.status = 'WORKING';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Plant on empty tilled soil (if has seeds)
|
||||
if (wd.inventory.seeds > 0) {
|
||||
for (let dx = -wd.radius; dx <= wd.radius; dx++) {
|
||||
for (let dy = -wd.radius; dy <= wd.radius; dy++) {
|
||||
const tile = terrain.getTile(wd.centerX + dx, wd.centerY + dy);
|
||||
if (tile && tile.isTilled && !tile.hasCrop) {
|
||||
farming.plant(wd.centerX + dx, wd.centerY + dy, 'wheat');
|
||||
wd.inventory.seeds--;
|
||||
console.log(`🧟🌱 Worker planted (Seeds: ${wd.inventory.seeds})`);
|
||||
wd.status = 'WORKING';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Till grass/dirt
|
||||
for (let dx = -wd.radius; dx <= wd.radius; dx++) {
|
||||
for (let dy = -wd.radius; dy <= wd.radius; dy++) {
|
||||
const tile = terrain.getTile(wd.centerX + dx, wd.centerY + dy);
|
||||
const typeName = tile?.type?.name || tile?.type;
|
||||
|
||||
if ((typeName === 'grass' || typeName === 'dirt') && !tile.isTilled) {
|
||||
tile.isTilled = true;
|
||||
if (tile.sprite) tile.sprite.setTint(0x8B4513);
|
||||
console.log(`🧟🔨 Worker tilled soil`);
|
||||
wd.status = 'WORKING';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wd.status = 'IDLE';
|
||||
}
|
||||
|
||||
performMineWork(zombie) {
|
||||
const wd = zombie.workerData;
|
||||
const terrain = this.scene.terrainSystem;
|
||||
if (!terrain || !terrain.decorationsMap) return;
|
||||
|
||||
for (let dx = -wd.radius; dx <= wd.radius; dx++) {
|
||||
for (let dy = -wd.radius; dy <= wd.radius; dy++) {
|
||||
const key = `${wd.centerX + dx},${wd.centerY + dy}`;
|
||||
if (terrain.decorationsMap.has(key)) {
|
||||
const decor = terrain.decorationsMap.get(key);
|
||||
if (decor.type === 'stone' || decor.type.includes('rock')) {
|
||||
terrain.removeDecoration(wd.centerX + dx, wd.centerY + dy);
|
||||
|
||||
if (this.scene.inventorySystem) {
|
||||
this.scene.inventorySystem.addItem('stone', 1);
|
||||
}
|
||||
|
||||
console.log(`🧟⛏️ Worker mined stone`);
|
||||
wd.status = 'WORKING';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wd.status = 'IDLE';
|
||||
}
|
||||
|
||||
performClearWork(zombie) {
|
||||
const wd = zombie.workerData;
|
||||
const terrain = this.scene.terrainSystem;
|
||||
if (!terrain || !terrain.decorationsMap) return;
|
||||
|
||||
for (let dx = -wd.radius; dx <= wd.radius; dx++) {
|
||||
for (let dy = -wd.radius; dy <= wd.radius; dy++) {
|
||||
const key = `${wd.centerX + dx},${wd.centerY + dy}`;
|
||||
if (terrain.decorationsMap.has(key)) {
|
||||
const decor = terrain.decorationsMap.get(key);
|
||||
const t = decor.type;
|
||||
|
||||
// Clear trees, bushes, logs, rocks
|
||||
if (t.startsWith('tree') || t.startsWith('bush') || t === 'fallen_log' || t === 'stone' || t.startsWith('small_rock')) {
|
||||
terrain.removeDecoration(wd.centerX + dx, wd.centerY + dy);
|
||||
|
||||
// Give some resources
|
||||
if (this.scene.inventorySystem) {
|
||||
if (t.startsWith('tree') || t === 'fallen_log' || t.startsWith('bush')) {
|
||||
this.scene.inventorySystem.addItem('wood', 1);
|
||||
} else {
|
||||
this.scene.inventorySystem.addItem('stone', 1);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🧟🪓 Worker CLEARED ${t}`);
|
||||
wd.status = 'WORKING';
|
||||
return; // One per tick
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
wd.status = 'IDLE';
|
||||
}
|
||||
|
||||
onWorkerDeath(zombie) {
|
||||
console.log(`💀 Worker died at ${zombie.gridX},${zombie.gridY}`);
|
||||
|
||||
if (this.scene.interactionSystem && this.scene.interactionSystem.spawnLoot) {
|
||||
this.scene.interactionSystem.spawnLoot(zombie.gridX, zombie.gridY, 'fertilizer', 1);
|
||||
}
|
||||
|
||||
if (this.scene.statsSystem) {
|
||||
this.scene.statsSystem.addXP(10);
|
||||
}
|
||||
|
||||
this.removeWorker(zombie);
|
||||
}
|
||||
|
||||
feedWorker(zombie, amount = 50) {
|
||||
if (!zombie.workerData) return false;
|
||||
|
||||
zombie.workerData.energy = Math.min(100, zombie.workerData.energy + amount);
|
||||
if (zombie.sprite) zombie.sprite.clearTint();
|
||||
|
||||
console.log(`🍖 Fed worker`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user