feat: Complete Terrain Setup & Camping Mechanics - Devlog 2026-01-27 08:46

This commit is contained in:
2026-01-27 08:46:54 +01:00
parent 51b3b67423
commit 82c992a94b
164 changed files with 596 additions and 9262 deletions

View File

@@ -2,8 +2,12 @@ import GrassScene from './scenes/GrassScene_Clean.js';
const config = {
type: Phaser.AUTO,
width: window.innerWidth,
height: window.innerHeight,
width: 1920,
height: 1080,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
},
backgroundColor: '#1a1a1a', // Temno siva, da slika izstopa
parent: 'body',
physics: {

View File

@@ -1,289 +1,295 @@
export default class GrassSceneClean extends Phaser.Scene {
preload() {
this.load.path = 'assets/DEMO_FAZA1/';
this.load.path = 'assets/';
// --- CHARACTERS ---
this.load.image('kai', 'Characters/Kai_Dreads.png');
// --- GROUND & ENVIRONMENT ---
this.load.image('ground_base', 'Ground/tla_trava_tekstura.png');
this.load.image('ground_dirt', 'Ground/ground_dirt_patch.png');
// --- 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');
// --- CORE ASSETS ---
this.load.image('trava_osnova', 'tiles/trava_osnova.png');
this.load.image('blato', 'environment/blato.png');
this.load.image('cesta', 'environment/cesta_svetla.png');
this.load.image('voda_cista', 'environment/voda_cista.png');
this.load.image('voda_umazana', 'environment/voda_umazana.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');
this.load.image('trava_rob', 'vegetation/trava_rob.png');
this.load.image('trava_zelena', 'vegetation/trava_zelena.png');
this.load.image('trava_suha', 'vegetation/trava_suha.png');
this.load.image('trava_divja', 'vegetation/trava_divja.png');
this.load.image('drevo', 'vegetation/drevo_navadno.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');
// --- CHARACTERS ---
this.load.image('kai', 'characters/kai.png');
// --- CAMP ITEMS ---
this.load.image('campfire', 'items/campfire.png');
this.load.image('sleeping_bag', 'items/sleeping_bag.png');
}
create() {
// 1. WORLD SETUP
const WORLD_W = 2000;
const WORLD_H = 2000;
const WORLD_W = 2500;
const WORLD_H = 2500;
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');
this.cameras.main.setBackgroundColor('#4a6b30');
// ZONES TRACKING (To prevent grass on water/objects)
// Array of {x, y, radius} or {rect}
this.restrictedZones = [];
// --- GROUPS ---
this.groundDecals = this.add.group();
this.environmentObjects = this.add.group(); // ALL Y-Sorted items here
// --- LAYER 0: OSNOVA ---
this.add.tileSprite(WORLD_W / 2, WORLD_H / 2, WORLD_W, WORLD_H, 'ground_base').setDepth(0);
// --- BASE LAYER (Scale 0.15) ---
let bg = this.add.tileSprite(WORLD_W / 2, WORLD_H / 2, WORLD_W, WORLD_H, 'trava_osnova');
bg.setTileScale(0.15);
bg.setDepth(-100);
bg.setTint(0xccffcc);
// --- LAYER 1: VODA & UMAZANIJA (Flat, "vtopljeno" v tla) ---
// Trava NE SME rasti tukaj
this.createGroundLiquids(WORLD_W);
// --- 1. PATH GENERATION ---
const pathPoints = [
{ x: 0, y: 500 }, { x: 400, y: 600 }, { x: 800, y: 800 },
{ x: 1000, y: 1200 }, { x: 1300, y: 1400 },
{ x: 1800, y: 1500 }, { x: 2200, y: 1800 }, { x: 2500, y: 2200 }
];
const curve = new Phaser.Curves.Spline(pathPoints);
const pathSpaced = curve.getSpacedPoints(80);
// --- 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 });
pathSpaced.forEach(p => {
let dirt = this.add.image(p.x, p.y, 'cesta');
dirt.setScale(0.8 + Math.random() * 0.3);
dirt.setRotation(Math.random() * 6.28);
dirt.setAlpha(0.8);
dirt.setBlendMode(Phaser.BlendModes.MULTIPLY);
dirt.setDepth(-50);
this.groundDecals.add(dirt);
this.addDensePathEdge(p.x, p.y);
});
// Place Major Objects (Clean Water, Hay, Thorns, Big Trees)
// with STRICT SPACING (100-150px)
this.createMajorObjects(WORLD_W, WORLD_H);
// --- 2. FLUIDS ---
this.createPond(800, 1800, 'voda_cista');
this.createPond(1800, 600, 'voda_cista');
this.createPond(500, 600, 'blato');
// --- VEGETATION FILL ---
// Fills the gaps, strictly respecting restrictedZones
this.populateVegetation(WORLD_W, WORLD_H);
// --- 3. GRASS SCATTER ---
const grassTypes = ['trava_zelena', 'trava_suha', 'trava_divja'];
const grassCount = 900;
// --- 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);
for (let i = 0; i < grassCount; i++) {
let x = Phaser.Math.Between(0, WORLD_W);
let y = Phaser.Math.Between(0, WORLD_H);
let safe = true;
for (let p of pathSpaced) { if (Phaser.Math.Distance.Between(x, y, p.x, p.y) < 70) { safe = false; break; } }
if (Phaser.Math.Distance.Between(x, y, 800, 1800) < 150) safe = false;
if (Phaser.Math.Distance.Between(x, y, 1800, 600) < 150) safe = false;
if (Phaser.Math.Distance.Between(x, y, 500, 600) < 100) safe = false;
// Scale to 64px height strict
if (safe) {
let type = grassTypes[Phaser.Math.Between(0, 2)];
let tuft = this.add.image(x, y, type);
tuft.setScale(0.15 + Math.random() * 0.15);
tuft.setOrigin(0.5, 0.9);
tuft.name = 'grass';
let tintChoice = Math.random();
if (tintChoice < 0.3) tuft.setTint(0x99cc99);
else if (tintChoice < 0.6) tuft.setTint(0x77aa77);
else tuft.setTint(0xccbb99);
this.environmentObjects.add(tuft);
}
}
// --- 4. TREES ---
const treePositions = [
{ x: 600, y: 1400 }, { x: 1400, y: 1400 },
{ x: 400, y: 1000 }, { x: 1600, y: 400 }
];
treePositions.forEach(pos => {
let tree = this.physics.add.image(pos.x, pos.y, 'drevo');
tree.setOrigin(0.5, 0.9);
tree.setImmovable(true);
tree.body.setCircle(30, tree.width / 2 - 30, tree.height - 40);
this.environmentObjects.add(tree);
});
// --- KAI ---
this.kai = this.physics.add.sprite(WORLD_W / 2, WORLD_H / 2, 'kai');
this.kai.setScale(64 / this.kai.height);
this.kai.setCollideWorldBounds(true);
this.kai.body.setSize(24, 20);
this.kai.body.setOffset(this.kai.width / 2 - 12, this.kai.height - 20);
this.kai.setOrigin(0.5, 0.9);
this.environmentObjects.add(this.kai);
this.physics.add.collider(this.kai, this.environmentObjects);
this.kai.setDepth(this.kai.y);
this.gameObjects.add(this.kai);
// Physics Colliders for Layer 2 objects
this.physics.add.collider(this.kai, this.hayGroup);
this.physics.add.collider(this.hayGroup, this.hayGroup);
// Camera
// --- CAMERA ---
this.cameras.main.startFollow(this.kai, true, 0.08, 0.08);
this.cameras.main.setZoom(1.3);
// AMNESIA VISUALS
// --- INPUTS ---
this.cursors = this.input.keyboard.createCursorKeys();
this.keySpace = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
this.keyC = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.C);
this.keyV = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.V);
// --- UI INSTRUCTIONS ---
this.add.text(20, 20, 'KONTROLE:\nPUŠČICE: Hoja\nSPACE: Košnja tave\nC: Kres\nV: Spalka', {
font: '16px Courier', fill: '#ffff00', backgroundColor: '#000000aa', padding: { x: 10, y: 10 }
}).setScrollFactor(0).setDepth(2000);
// --- AMNESIA INTRO ---
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' });
this.blurEffect = this.cameras.main.postFX.addBlur(0, 2, 2, 1.0);
this.blurEffect.strength = 10;
const fullText = "Kje sta starša...?";
let txt = this.add.text(this.cameras.main.width / 2, this.cameras.main.height / 2, '', {
fontFamily: 'Courier', fontSize: '32px', color: '#ffffff', fontStyle: 'bold',
shadow: { offsetX: 2, offsetY: 2, color: '#000', blur: 4, stroke: true, fill: true }
});
txt.setOrigin(0.5);
txt.setScrollFactor(0);
txt.setDepth(1000);
let charIndex = 0;
this.time.addEvent({
delay: 150, repeat: fullText.length - 1,
callback: () => { txt.text += fullText[charIndex]; charIndex++; }
});
this.input.once('pointerdown', () => {
this.tweens.add({ targets: this.blurEffect, strength: 0, duration: 4000, ease: 'Power2' });
this.tweens.add({ targets: txt, alpha: 0, duration: 1000, ease: 'Power2' });
});
}
console.log("Scene Fully Rebuilt.");
}
addDensePathEdge(x, y) {
for (let i = 0; i < 3; i++) {
let offset = 50 + Math.random() * 20;
if (Math.random() > 0.5) offset *= -1;
let ang = (Math.random() - 0.5) * 1.5;
let tx = x + Math.cos(ang) * offset;
let ty = y + Math.sin(ang) * offset;
let tuft = this.add.image(tx, ty, 'trava_rob');
tuft.setScale(0.12 + Math.random() * 0.1);
tuft.setOrigin(0.5, 0.9);
tuft.setTint(0xaaaaaa);
this.environmentObjects.add(tuft);
}
}
createPond(x, y, type) {
let pond = this.add.image(x, y, type);
pond.setScale(0.7);
pond.setDepth(-50);
pond.setBlendMode(Phaser.BlendModes.MULTIPLY);
this.groundDecals.add(pond);
let circum = Math.PI * (pond.width * 0.7);
let count = Math.floor(circum / 25);
for (let i = 0; i < count; i++) {
let a = (i / count) * Math.PI * 2;
let r = (pond.width * 0.35) - 5;
let tx = x + Math.cos(a) * r;
let ty = y + Math.sin(a) * r;
let tuft = this.add.image(tx, ty, 'trava_rob');
tuft.setScale(0.2 + Math.random() * 0.15);
tuft.setOrigin(0.5, 0.9);
tuft.setTint(0xaaaaaa);
this.environmentObjects.add(tuft);
}
}
cutGrass(grass) {
if (grass.isCutting) return;
grass.isCutting = true;
this.tweens.add({
targets: grass, scaleY: 0, scaleX: 0.1, alpha: 0.5, y: grass.y + 10, duration: 200,
onComplete: () => { grass.destroy(); }
});
}
placeItem(type) {
let x = this.kai.x;
let y = this.kai.y + 20;
let valid = true;
// Check Fluids
this.groundDecals.children.each(child => {
if (child.texture.key.includes('blato') || child.texture.key.includes('voda')) {
if (Phaser.Math.Distance.Between(x, y, child.x, child.y) < (child.width * 0.35)) valid = false;
}
});
// Check Grass/Trees
this.environmentObjects.children.each(child => {
if (child.name === 'grass' && child.active) {
if (Phaser.Math.Distance.Between(x, y, child.x, child.y) < 30) valid = false;
}
if (child.texture.key === 'drevo') {
if (Phaser.Math.Distance.Between(x, y, child.x, child.y) < 50) valid = false;
}
});
if (valid) {
let item = this.physics.add.image(x, y, type);
item.setOrigin(0.5, 0.9);
this.environmentObjects.add(item);
// Animation
item.setScale(0);
this.tweens.add({ targets: item, scaleX: 1, scaleY: 1, duration: 400, ease: 'Back' });
// Text feedback
let fdb = this.add.text(x, y - 50, 'Postavljeno!', { fontSize: '14px', color: '#ffff00', stroke: '#000', strokeThickness: 2 }).setOrigin(0.5);
this.tweens.add({ targets: fdb, y: y - 80, alpha: 0, duration: 1000, onComplete: () => fdb.destroy() });
} else {
this.cameras.main.fadeIn(5000);
let fdb = this.add.text(x, y - 50, 'Najprej pokosi travo!', { fontSize: '14px', color: '#ff0000', stroke: '#000', strokeThickness: 2 }).setOrigin(0.5);
this.tweens.add({ targets: fdb, y: y - 80, alpha: 0, duration: 1000, onComplete: () => fdb.destroy() });
this.cameras.main.shake(100, 0.005);
}
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 });
}
// 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);
}
}
// 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);
}
}
// 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);
}
}
}
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);
});
this.environmentObjects.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
const speed = 250;
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);
if (this.cursors.left.isDown) this.kai.setVelocityX(-speed);
else if (this.cursors.right.isDown) this.kai.setVelocityX(speed);
if (this.cursors.up.isDown) this.kai.setVelocityY(-speed);
else if (this.cursors.down.isDown) this.kai.setVelocityY(speed);
this.kai.body.velocity.normalize().scale(speed);
// Mowing
if (this.keySpace.isDown) {
let range = 60;
this.environmentObjects.children.each(child => {
if (child.name === 'grass' && child.active) {
if (Phaser.Math.Distance.Between(this.kai.x, this.kai.y, child.x, child.y) < range) {
this.cutGrass(child);
}
}
});
}
// Place Campfire/Sleeping Bag
if (Phaser.Input.Keyboard.JustDown(this.keyC)) this.placeItem('campfire');
if (Phaser.Input.Keyboard.JustDown(this.keyV)) this.placeItem('sleeping_bag');
// Sinking/Tint
let inFluid = false;
this.groundDecals.children.each(child => {
if (child.texture.key === 'blato' || child.texture.key.includes('voda')) {
if (Phaser.Math.Distance.Between(this.kai.x, this.kai.y, child.x, child.y) < (child.width * 0.35)) {
inFluid = true;
}
}
});
if (inFluid) {
this.kai.setOrigin(0.5, 0.8);
this.kai.setTint(0xdddddd);
} else {
this.kai.setOrigin(0.5, 0.9);
this.kai.clearTint();
}
}
}