Implement Nova Farma S1 Max: Layered terrain, water mechanics, Y-sorting, and asset cleanup
This commit is contained in:
@@ -1,216 +1,289 @@
|
||||
export default class GrassScene extends Phaser.Scene {
|
||||
constructor() {
|
||||
super({ key: 'GrassScene' });
|
||||
this.baseTime = 12;
|
||||
this.timeSpeed = 0.5;
|
||||
this.playerSpeed = 200;
|
||||
this.hayCount = 0;
|
||||
}
|
||||
|
||||
export default class GrassSceneClean extends Phaser.Scene {
|
||||
preload() {
|
||||
console.log("🌿 Loading Clean Assets...");
|
||||
this.load.path = 'assets/DEMO_FAZA1/';
|
||||
|
||||
const PATHS = {
|
||||
ground: 'assets/DEMO_FAZA1/Ground/',
|
||||
veg: 'assets/DEMO_FAZA1/Vegetation/',
|
||||
char: 'assets/references/',
|
||||
items: 'assets/DEMO_FAZA1/Items/Hay/',
|
||||
audio: 'assets/audio/_NEW/'
|
||||
};
|
||||
// --- CHARACTERS ---
|
||||
this.load.image('kai', 'Characters/Kai_Dreads.png');
|
||||
|
||||
this.load.image('ground_base', PATHS.ground + 'tla_trava_tekstura.png');
|
||||
this.load.image('grass_tall', PATHS.veg + 'visoka_trava.png');
|
||||
this.load.image('hay_drop', PATHS.items + 'hay_drop_0.png');
|
||||
this.load.image('kai', PATHS.char + 'kaj.png');
|
||||
this.load.audio('step_grass', PATHS.audio + 'footstep_grass_000.ogg');
|
||||
// --- GROUND & ENVIRONMENT ---
|
||||
this.load.image('ground_base', 'Ground/tla_trava_tekstura.png');
|
||||
this.load.image('ground_dirt', 'Ground/ground_dirt_patch.png');
|
||||
|
||||
this.load.on('loaderror', (file) => {
|
||||
console.error('FAILED TO LOAD:', file.src);
|
||||
});
|
||||
// --- WATER BIOMES ---
|
||||
// Layer 1 (Flat/Dirty)
|
||||
this.load.image('puddle_mud', 'Environment/mud_puddle.png');
|
||||
this.load.image('stream', 'Environment/stream_water.png');
|
||||
|
||||
// Layer 2 (Interactive/Structure)
|
||||
this.load.image('water_clean', 'Environment/water_clean_patch.png');
|
||||
|
||||
// --- VEGETATION ---
|
||||
this.load.image('grass_low', 'Vegetation/trava_sop.png');
|
||||
this.load.image('grass_high', 'Vegetation/visoka_trava_v2.png');
|
||||
this.load.image('grass_dense', 'Vegetation/grass_cluster_dense.png');
|
||||
|
||||
// --- OBSTACLES ---
|
||||
this.load.image('thorns', 'Environment/obstacle_thorns.png');
|
||||
this.load.image('hay', 'Items/hay_drop_0.png');
|
||||
this.load.image('tree_big', 'Vegetation/drevo_veliko.png');
|
||||
}
|
||||
|
||||
create() {
|
||||
const { width, height } = this.scale;
|
||||
// 1. WORLD SETUP
|
||||
const WORLD_W = 2000;
|
||||
const WORLD_H = 2000;
|
||||
this.physics.world.setBounds(0, 0, WORLD_W, WORLD_H);
|
||||
this.cameras.main.setBounds(0, 0, WORLD_W, WORLD_H);
|
||||
this.cameras.main.setBackgroundColor('#1a3300');
|
||||
|
||||
// 1. BACKGROUND
|
||||
this.ground = this.add.tileSprite(0, 0, width, height, 'ground_base')
|
||||
.setOrigin(0, 0);
|
||||
// ZONES TRACKING (To prevent grass on water/objects)
|
||||
// Array of {x, y, radius} or {rect}
|
||||
this.restrictedZones = [];
|
||||
|
||||
// 2. GROUPS
|
||||
this.tallGrassGroup = this.physics.add.group();
|
||||
this.hayGroup = this.physics.add.group();
|
||||
// --- LAYER 0: OSNOVA ---
|
||||
this.add.tileSprite(WORLD_W / 2, WORLD_H / 2, WORLD_W, WORLD_H, 'ground_base').setDepth(0);
|
||||
|
||||
this.allGrass = [];
|
||||
// --- LAYER 1: VODA & UMAZANIJA (Flat, "vtopljeno" v tla) ---
|
||||
// Trava NE SME rasti tukaj
|
||||
this.createGroundLiquids(WORLD_W);
|
||||
|
||||
// 3. GENERATE WORLD - ONLY GRASS
|
||||
// "Vsa drevesa odstranjena. Samo trava ostane."
|
||||
// "Nastavi neskončno travo čez celo mapo"
|
||||
this.generateInfiniteGrass(width, height);
|
||||
// --- LAYER 2: INTERAKTIVNI OBJEKTI & Y-SORT GROUP ---
|
||||
this.gameObjects = this.add.group({ runChildUpdate: true });
|
||||
this.obstacles = this.physics.add.group(); // For thorns interaction
|
||||
this.hayGroup = this.physics.add.group({ bounceX: 0.2, bounceY: 0.2, dragX: 100, dragY: 100 });
|
||||
|
||||
// 4. KAI (Player)
|
||||
this.player = this.physics.add.sprite(width / 2, height / 2, 'kai');
|
||||
this.player.setScale(0.5);
|
||||
this.player.setOrigin(0.5, 0.92);
|
||||
this.player.setCollideWorldBounds(true);
|
||||
this.player.setDepth(this.player.y);
|
||||
// Place Major Objects (Clean Water, Hay, Thorns, Big Trees)
|
||||
// with STRICT SPACING (100-150px)
|
||||
this.createMajorObjects(WORLD_W, WORLD_H);
|
||||
|
||||
this.lastStepTime = 0;
|
||||
// --- VEGETATION FILL ---
|
||||
// Fills the gaps, strictly respecting restrictedZones
|
||||
this.populateVegetation(WORLD_W, WORLD_H);
|
||||
|
||||
// 5. INTERACTION LOGIC
|
||||
this.physics.add.overlap(this.player, this.tallGrassGroup, this.handleMow, null, this);
|
||||
this.physics.add.overlap(this.player, this.hayGroup, this.collectHay, null, this);
|
||||
// --- PLAYER (KAI) ---
|
||||
this.kai = this.physics.add.sprite(1000, 1000, 'kai');
|
||||
this.kai.setCollideWorldBounds(true);
|
||||
this.kai.body.setSize(30, 30);
|
||||
this.kai.body.setOffset(this.kai.width / 2 - 15, this.kai.height - 30);
|
||||
|
||||
// Input
|
||||
this.cursors = this.input.keyboard.createCursorKeys();
|
||||
this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
|
||||
// Scale to 64px height strict
|
||||
this.kai.setScale(64 / this.kai.height);
|
||||
|
||||
// 6. AMNESIA FADE IN
|
||||
this.cameras.main.fadeIn(3000, 0, 0, 0);
|
||||
this.kai.setDepth(this.kai.y);
|
||||
this.gameObjects.add(this.kai);
|
||||
|
||||
// DYNAMIC VIGNETTE
|
||||
const canvasTexture = this.textures.createCanvas('vignette_tex', width, height);
|
||||
const ctx = canvasTexture.context;
|
||||
const grd = ctx.createRadialGradient(width / 2, height / 2, height * 0.3, width / 2, height / 2, height * 0.8);
|
||||
grd.addColorStop(0, "rgba(0,0,0,0)");
|
||||
grd.addColorStop(1, "rgba(0,0,0,0.9)");
|
||||
// Physics Colliders for Layer 2 objects
|
||||
this.physics.add.collider(this.kai, this.hayGroup);
|
||||
this.physics.add.collider(this.hayGroup, this.hayGroup);
|
||||
|
||||
ctx.fillStyle = grd;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
canvasTexture.refresh();
|
||||
// Camera
|
||||
this.cameras.main.startFollow(this.kai, true, 0.08, 0.08);
|
||||
|
||||
this.vignette = this.add.image(width / 2, height / 2, 'vignette_tex')
|
||||
.setScrollFactor(0)
|
||||
.setAlpha(0)
|
||||
.setDepth(8000);
|
||||
|
||||
// 7. DAY/NIGHT OVERLAY
|
||||
this.dayNightOverlay = this.add.rectangle(0, 0, width, height, 0x000000, 0)
|
||||
.setOrigin(0, 0)
|
||||
.setDepth(9000);
|
||||
|
||||
// 8. UI INFO
|
||||
this.infoText = this.add.text(20, 20, '', {
|
||||
fontFamily: 'monospace', fontSize: '20px', fill: '#ffffff', stroke: '#000000', strokeThickness: 4
|
||||
}).setDepth(10000);
|
||||
|
||||
this.inventoryText = this.add.text(width - 150, 20, 'Seno: 0', {
|
||||
fontFamily: 'monospace', fontSize: '20px', fill: '#ffff00', stroke: '#000000', strokeThickness: 4
|
||||
}).setDepth(10000);
|
||||
}
|
||||
|
||||
generateInfiniteGrass(w, h) {
|
||||
// "Density = 1.0f" -> High density, cover everything.
|
||||
// We'll use a grid loop with some randomization offset
|
||||
// Grass scale is 0.2, original image is ~512?
|
||||
// 0.2 * 512 = ~100px width.
|
||||
// Step size ~50px for overlap.
|
||||
|
||||
const stepX = 40;
|
||||
const stepY = 30;
|
||||
|
||||
for (let y = 0; y < h; y += stepY) {
|
||||
for (let x = 0; x < w; x += stepX) {
|
||||
// Add some randomness
|
||||
let gx = x + Phaser.Math.Between(-10, 10);
|
||||
let gy = y + Phaser.Math.Between(-10, 10);
|
||||
|
||||
// create(x, y, key)
|
||||
let grass = this.tallGrassGroup.create(gx, gy, 'grass_tall');
|
||||
grass.setScale(0.2);
|
||||
grass.setOrigin(0.5, 0.95);
|
||||
grass.setDepth(gy + 1);
|
||||
|
||||
grass.swaySpeed = 0.002 + Math.random() * 0.001;
|
||||
grass.swayOffset = Math.random() * 100;
|
||||
|
||||
this.allGrass.push(grass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleMow(player, grass) {
|
||||
if (this.spaceKey.isDown) {
|
||||
let hay = this.hayGroup.create(grass.x, grass.y + 10, 'hay_drop');
|
||||
hay.setScale(0.6);
|
||||
hay.setDepth(1);
|
||||
hay.setAngle(Phaser.Math.Between(-20, 20));
|
||||
grass.destroy();
|
||||
const index = this.allGrass.indexOf(grass);
|
||||
if (index > -1) this.allGrass.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
collectHay(player, hay) {
|
||||
hay.destroy();
|
||||
this.hayCount++;
|
||||
this.inventoryText.setText(`Seno: ${this.hayCount}`);
|
||||
}
|
||||
|
||||
update(time, delta) {
|
||||
// MOVEMENT LOGIC
|
||||
let currentSpeed = this.playerSpeed;
|
||||
let isMoving = false;
|
||||
|
||||
// HIDING CHECK (Always effectively in grass in this mode, but let's check overlap technically)
|
||||
let inTallGrass = this.physics.overlap(this.player, this.tallGrassGroup);
|
||||
|
||||
if (inTallGrass) {
|
||||
currentSpeed *= 0.6;
|
||||
// "Player.CharacterAlpha = 0.4f"
|
||||
this.player.setAlpha(0.4);
|
||||
if (this.vignette.alpha < 1) this.vignette.alpha += 0.05;
|
||||
// AMNESIA VISUALS
|
||||
if (this.cameras.main.postFX) {
|
||||
const blur = this.cameras.main.postFX.addBlur(20, 2, 2, 1.0);
|
||||
this.tweens.add({ targets: blur, strength: 0, duration: 5000, ease: 'Power2' });
|
||||
} else {
|
||||
// If he mows a clearing, he becomes visible
|
||||
this.player.setAlpha(1.0);
|
||||
if (this.vignette.alpha > 0) this.vignette.alpha -= 0.05;
|
||||
this.cameras.main.fadeIn(5000);
|
||||
}
|
||||
|
||||
this.player.setVelocity(0);
|
||||
if (this.cursors.left.isDown) {
|
||||
this.player.setVelocityX(-currentSpeed);
|
||||
this.player.setFlipX(true);
|
||||
isMoving = true;
|
||||
} else if (this.cursors.right.isDown) {
|
||||
this.player.setVelocityX(currentSpeed);
|
||||
this.player.setFlipX(false);
|
||||
isMoving = true;
|
||||
}
|
||||
if (this.cursors.up.isDown) {
|
||||
this.player.setVelocityY(-currentSpeed);
|
||||
isMoving = true;
|
||||
} else if (this.cursors.down.isDown) {
|
||||
this.player.setVelocityY(currentSpeed);
|
||||
isMoving = true;
|
||||
console.log("Nova Farma: Logical Layering Active.");
|
||||
}
|
||||
|
||||
createGroundLiquids(w) {
|
||||
// Mud Puddles (Flat)
|
||||
for (let i = 0; i < 8; i++) {
|
||||
let x = Phaser.Math.Between(200, 1800);
|
||||
let y = Phaser.Math.Between(200, 1800);
|
||||
// Safe zone check
|
||||
if (Phaser.Math.Distance.Between(x, y, 1000, 1000) < 300) continue;
|
||||
|
||||
let mud = this.add.image(x, y, 'puddle_mud');
|
||||
mud.setDepth(0.01); // Layer 1
|
||||
mud.setScale(1.2);
|
||||
|
||||
// Register zone (exclude grass here)
|
||||
// Radius approx width/2 * scale
|
||||
this.restrictedZones.push({ x: x, y: y, radius: (mud.width / 2 * 1.2) - 10 });
|
||||
}
|
||||
|
||||
// AUDIO
|
||||
if (isMoving && inTallGrass) {
|
||||
if (time > this.lastStepTime + 350) {
|
||||
this.sound.play('step_grass', { volume: 0.4 });
|
||||
this.lastStepTime = time;
|
||||
// Stream (Flat)
|
||||
for (let i = 0; i < 5; i++) {
|
||||
let x = 300 + (i * 250);
|
||||
let y = 1600 + Math.sin(i) * 60;
|
||||
let stream = this.add.image(x, y, 'stream');
|
||||
stream.setDepth(0.01);
|
||||
stream.setRotation(0.1);
|
||||
|
||||
this.restrictedZones.push({ x: x, y: y, radius: 100 });
|
||||
}
|
||||
}
|
||||
|
||||
createMajorObjects(w, h) {
|
||||
// We will maintain a list of major object positions to enforce the 150px grid spacing
|
||||
// restrictedZones handles the water overlap, duplicate this list for object spacing if needed
|
||||
// but adding to restrictedZones works for both.
|
||||
|
||||
// 1. Clean Water Panel (Interactive, Y-Sorted)
|
||||
let wx = 1150, wy = 950;
|
||||
let wClean = this.add.image(wx, wy, 'water_clean');
|
||||
wClean.setOrigin(0.5, 0.9);
|
||||
wClean.setScale(0.7); // Slightly smaller
|
||||
this.restrictedZones.push({ x: wx, y: wy, radius: 80 });
|
||||
this.gameObjects.add(wClean);
|
||||
|
||||
// 2. Thorns
|
||||
// ... (Similar logic as before, ensure spacing)
|
||||
for (let i = 0; i < 12; i++) {
|
||||
let pos = this.findFreeSpot(w, h, 150);
|
||||
if (pos) {
|
||||
let thorn = this.obstacles.create(pos.x, pos.y, 'thorns');
|
||||
thorn.setOrigin(0.5, 0.82);
|
||||
thorn.body.setCircle(30);
|
||||
thorn.body.setOffset(thorn.width / 2 - 30, thorn.height - 40);
|
||||
thorn.setImmovable(true);
|
||||
this.restrictedZones.push({ x: pos.x, y: pos.y, radius: 80 });
|
||||
this.gameObjects.add(thorn);
|
||||
}
|
||||
}
|
||||
|
||||
// VISUALS
|
||||
this.player.setDepth(this.player.y);
|
||||
this.allGrass.forEach(g => { if (g.active) g.rotation = Math.sin((time * g.swaySpeed) + g.swayOffset) * 0.15; });
|
||||
// 3. Hay
|
||||
for (let i = 0; i < 6; i++) {
|
||||
let pos = this.findFreeSpot(w, h, 100);
|
||||
if (pos) {
|
||||
let hay = this.hayGroup.create(pos.x, pos.y, 'hay');
|
||||
hay.setOrigin(0.5, 0.85);
|
||||
hay.setScale(0.8);
|
||||
hay.body.setCircle(15);
|
||||
hay.body.setOffset(hay.width / 2 - 15, hay.height - 20);
|
||||
this.restrictedZones.push({ x: pos.x, y: pos.y, radius: 50 });
|
||||
this.gameObjects.add(hay);
|
||||
}
|
||||
}
|
||||
|
||||
this.baseTime += (delta * 0.001 * this.timeSpeed);
|
||||
if (this.baseTime >= 24) this.baseTime = 0;
|
||||
this.updateLighting(this.baseTime);
|
||||
|
||||
let hour = Math.floor(this.baseTime);
|
||||
let minute = Math.floor((this.baseTime % 1) * 60).toString().padStart(2, '0');
|
||||
this.infoText.setText(`Time: ${hour}:${minute}\n[Arrows] Premik\n[Space] Košnja`);
|
||||
// 4. BIG TREES
|
||||
for (let i = 0; i < 10; i++) {
|
||||
let pos = this.findFreeSpot(w, h, 200);
|
||||
if (pos) {
|
||||
let tree = this.add.image(pos.x, pos.y, 'tree_big');
|
||||
tree.setOrigin(0.5, 0.92);
|
||||
tree.setScale(1.3);
|
||||
this.restrictedZones.push({ x: pos.x, y: pos.y, radius: 100 });
|
||||
this.gameObjects.add(tree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateLighting(hour) {
|
||||
let alpha = 0;
|
||||
let color = 0x000000;
|
||||
if (hour >= 20 || hour < 5) { alpha = 0.7; color = 0x000022; }
|
||||
else if (hour >= 5 && hour < 8) { alpha = 0.3; color = 0xFF4500; }
|
||||
else if (hour >= 18 && hour < 20) { alpha = 0.4; color = 0xFF4500; }
|
||||
else { alpha = 0; }
|
||||
this.dayNightOverlay.setFillStyle(color, alpha);
|
||||
populateVegetation(w, h) {
|
||||
// Fill gaps with grass
|
||||
// We use a looser spacing for grass (density) but strict check against restrictedZones
|
||||
|
||||
for (let i = 0; i < 400; i++) {
|
||||
let x = Phaser.Math.Between(50, w - 50);
|
||||
let y = Phaser.Math.Between(50, h - 50);
|
||||
|
||||
// Check strict zones (Water, Thorns, Trees)
|
||||
if (this.isZoneRestricted(x, y)) continue;
|
||||
|
||||
let type = '';
|
||||
let scale = 1.0;
|
||||
let rng = Math.random();
|
||||
|
||||
if (rng > 0.9) {
|
||||
// Dense Hiding Grass
|
||||
type = 'grass_dense';
|
||||
} else if (rng > 0.6) {
|
||||
type = 'grass_high';
|
||||
} else {
|
||||
type = 'grass_low';
|
||||
}
|
||||
|
||||
let grass = this.add.image(x, y, type);
|
||||
grass.setOrigin(0.5, 1.0); // Pivot Bottom for sorting
|
||||
|
||||
// Scale logic: Knee to Waist height
|
||||
// Kai ~64px. Grass ~30-45px.
|
||||
let hTarget = 25 + Math.random() * 20;
|
||||
grass.setScale(hTarget / grass.height);
|
||||
|
||||
// Tint blend
|
||||
grass.setTint(0xcccccc);
|
||||
|
||||
this.gameObjects.add(grass);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Find spot with minimal distance to others
|
||||
findFreeSpot(w, h, minDistance) {
|
||||
for (let i = 0; i < 50; i++) { // 50 attempts
|
||||
let x = Phaser.Math.Between(100, w - 100);
|
||||
let y = Phaser.Math.Between(100, h - 100);
|
||||
|
||||
// Safe spawn area check
|
||||
if (Phaser.Math.Distance.Between(x, y, 1000, 1000) < 300) continue;
|
||||
|
||||
// Check specific restrictions
|
||||
let ok = true;
|
||||
for (let z of this.restrictedZones) {
|
||||
if (Phaser.Math.Distance.Between(x, y, z.x, z.y) < (z.radius + minDistance)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ok) return { x, y };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
isZoneRestricted(x, y) {
|
||||
for (let z of this.restrictedZones) {
|
||||
// For grass, we just need to be outside the radius of the object (water/tree)
|
||||
if (Phaser.Math.Distance.Between(x, y, z.x, z.y) < z.radius) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
update() {
|
||||
// 1. GLOBAL DEPTH & Y-SORT
|
||||
this.gameObjects.children.each(child => {
|
||||
child.setDepth(child.y);
|
||||
});
|
||||
|
||||
// 2. MOVEMENT & PHYSICS
|
||||
const cursors = this.input.keyboard.createCursorKeys();
|
||||
let speed = 200;
|
||||
|
||||
// Interaction Checks
|
||||
let inThorns = false;
|
||||
let inMud = false;
|
||||
|
||||
// Thorns
|
||||
this.physics.overlap(this.kai, this.obstacles, () => inThorns = true);
|
||||
|
||||
// Mud (Sinking Effect)
|
||||
this.physics.overlap(this.kai, this.mudGroup, () => inMud = true);
|
||||
|
||||
// Apply Effects
|
||||
if (inThorns) {
|
||||
speed = 100;
|
||||
this.kai.setTint(0xffaaaa);
|
||||
} else {
|
||||
this.kai.clearTint();
|
||||
}
|
||||
|
||||
if (inMud) {
|
||||
this.kai.setAlpha(0.8); // "Ugrez" simulation
|
||||
} else {
|
||||
this.kai.setAlpha(1.0);
|
||||
}
|
||||
|
||||
// Velocity
|
||||
this.kai.setVelocity(0);
|
||||
if (cursors.left.isDown) this.kai.setVelocityX(-speed);
|
||||
else if (cursors.right.isDown) this.kai.setVelocityX(speed);
|
||||
if (cursors.up.isDown) this.kai.setVelocityY(-speed);
|
||||
else if (cursors.down.isDown) this.kai.setVelocityY(speed);
|
||||
|
||||
this.kai.body.velocity.normalize().scale(speed);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user