diff --git a/assets/environment/potok_segment.png b/assets/environment/potok_segment.png new file mode 100644 index 000000000..256865505 Binary files /dev/null and b/assets/environment/potok_segment.png differ diff --git a/scripts/crop_stream.py b/scripts/crop_stream.py new file mode 100644 index 000000000..79a3b7302 --- /dev/null +++ b/scripts/crop_stream.py @@ -0,0 +1,31 @@ +import cv2 +import numpy as np + +def crop_stream_segment(): + # Load the processed stream segment + img = cv2.imread("assets/environment/potok_segment.png", cv2.IMREAD_UNCHANGED) + + if img is None: + print("Error: Could not load image.") + return + + h, w = img.shape[:2] + + # The image is an isometric block. The bottom part is the "front wall". + # To make it look "sunken" or flat, we should remove the bottom front dirt wall. + # Let's interactively guess: removing the bottom 1/3 might work. + + # Crop top 75% (Remove bottom 25%) + # Adjust this factor based on visual inspection of "cube" height vs "face" height + # Ideally, we keep the top surface. + + crop_height = int(h * 0.70) + + # Crop + cropped = img[0:crop_height, 0:w] + + # Save + cv2.imwrite("assets/environment/potok_segment.png", cropped) + print(f"Cropped image from {h} to {crop_height} height.") + +crop_stream_segment() diff --git a/src/scenes/GrassScene_Clean.js b/src/scenes/GrassScene_Clean.js index d6a125a96..4503e0cbb 100644 --- a/src/scenes/GrassScene_Clean.js +++ b/src/scenes/GrassScene_Clean.js @@ -1,4 +1,8 @@ export default class GrassSceneClean extends Phaser.Scene { + constructor() { + super({ key: 'GrassSceneClean' }); + } + preload() { this.load.path = 'assets/'; @@ -7,21 +11,18 @@ export default class GrassSceneClean extends Phaser.Scene { 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'); + this.load.image('potok_segment', 'environment/potok_segment.png'); // --- VEGETATION --- - 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'); - - // --- 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'); + + // Eraser brush (dynamically created if needed, or use blato) } create() { @@ -29,264 +30,131 @@ export default class GrassSceneClean extends Phaser.Scene { 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('#4a6b30'); + this.cameras.main.setBackgroundColor('#2e3b20'); // Dark earthy background for the "hole" // --- GROUPS --- - this.groundDecals = this.add.group(); - this.environmentObjects = this.add.group(); // ALL Y-Sorted items here + this.waterLayer = this.add.group(); // Z: 0 + this.groundLayer = this.add.group(); // Z: 1 (The Masked Layer) + this.objectLayer = this.add.group(); // Z: 2 - // --- 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); + // --- 1. WATER LAYER (The "Bottom" of the Trench) --- + // We calculate the path first so we know where to put water + const startX = 0, startY = 800; + const endX = 2500, endY = 1800; + const dist = Phaser.Math.Distance.Between(startX, startY, endX, endY); + const count = Math.ceil(dist / 60); // High density for smoothness - // --- 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); + // Place water along the path (at depth 0) + for (let i = 0; i <= count; i++) { + let t = i / count; + let x = Phaser.Math.Linear(startX, endX, t); + let y = Phaser.Math.Linear(startY, endY, t); + y += Math.sin(t * 12) * 60; // Meander - 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); - }); + // Water Segment + let seg = this.add.image(x, y, 'potok_segment'); + seg.setScale(0.9); + seg.setDepth(1); // Layer 1 internally + seg.setAlpha(0.7); // Transparency + seg.setBlendMode(Phaser.BlendModes.NORMAL); + this.waterLayer.add(seg); - // --- 2. FLUIDS --- - this.createPond(800, 1800, 'voda_cista'); - this.createPond(1800, 600, 'voda_cista'); - this.createPond(500, 600, 'blato'); - - // --- 3. GRASS SCATTER --- - const grassTypes = ['trava_zelena', 'trava_suha', 'trava_divja']; - const grassCount = 900; - - 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; - - 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); - } + // Mud Underlay (Darker depth) + let mud = this.add.image(x, y + 10, 'blato'); + mud.setScale(1.0); + mud.setTint(0x1a1a0d); // Very dark/black + mud.setDepth(0); // Layer 0 + this.waterLayer.add(mud); } - // --- 4. TREES --- - const treePositions = [ - { x: 600, y: 1400 }, { x: 1400, y: 1400 }, - { x: 400, y: 1000 }, { x: 1600, y: 400 } - ]; + // --- 2. GROUND LAYER (The "Top" with a Hole) --- + // We use a RenderTexture to "Stamp out" the river + let rt = this.add.renderTexture(0, 0, WORLD_W, WORLD_H); + rt.setDepth(100); // Visually above water - 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); - }); + // Fill RT with the grass tile texture + // Since clear+fill with tileSprite isn't direct in RT, we draw a huge tileSprite once + let hugeBg = this.make.tileSprite({ x: WORLD_W / 2, y: WORLD_H / 2, width: WORLD_W, height: WORLD_H, key: 'trava_osnova' }, false); + hugeBg.setTileScale(0.15); + hugeBg.setTint(0xccffcc); + rt.draw(hugeBg, WORLD_W / 2, WORLD_H / 2); - // --- KAI --- + // NOW ERASE THE RIVER CHANNEL + // "Subtract Mask" logic using erase() + for (let i = 0; i <= count; i++) { + let t = i / count; + let x = Phaser.Math.Linear(startX, endX, t); + let y = Phaser.Math.Linear(startY, endY, t); + y += Math.sin(t * 12) * 60; + + // We use 'blato' sprite as an eraser brush because it has a soft alpha shape + let eraser = this.make.image({ x: 0, y: 0, key: 'blato' }, false); + eraser.setScale(0.7); // Slightly narrower than water to show bank edge? + // Actually, if we erase slightly LESS than the water width, the water "slides under" the ground -> Bank Effect! + + rt.erase(eraser, x, y); + + // Add a visual "Shadow" on top of the cut edge to simulate depth + let shadow = this.add.image(x, y - 5, 'blato'); + shadow.setScale(0.8); + shadow.setTint(0x000000); + shadow.setAlpha(0.4); + shadow.setDepth(101); // Right on top of the RT hole + shadow.setBlendMode(Phaser.BlendModes.MULTIPLY); + // This shadow needs to be masked too? No, it sits in the trench. + this.waterLayer.add(shadow); + } + + // --- 3. OBJECT LAYER (Y-Sorted) --- 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.objectLayer.add(this.kai); - // --- CAMERA --- + // Camera this.cameras.main.startFollow(this.kai, true, 0.08, 0.08); this.cameras.main.setZoom(1.3); - // --- INPUTS --- + // Control Keys 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 } + // Debug Text + this.add.text(20, 20, 'VISUAL FIX APPLIED:\nSubtraction Mask (RenderTexture Erase)', { + font: '16px Monospace', fill: '#00ff00', backgroundColor: '#000000aa' }).setScrollFactor(0).setDepth(2000); - - // --- AMNESIA INTRO --- - if (this.cameras.main.postFX) { - 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 { - 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); - } } update() { - this.environmentObjects.children.each(child => child.setDepth(child.y)); + // Depth Sort Objects + this.objectLayer.children.each(child => { + child.setDepth(child.y + 2000); // Ensure objects are always above the ground RT (Depth 100) + }); + // Controls const speed = 250; this.kai.setVelocity(0); - 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; - } + // Sunken Legs Logic + // Check overlap with water layer images (crude but effective) + let inWater = false; + this.waterLayer.children.each(w => { + if (this.physics.world.overlap(this.kai, w)) { + // Wait, water images don't have bodies. Use distance. + if (Phaser.Math.Distance.Between(this.kai.x, this.kai.y, w.x, w.y) < 40) inWater = true; } }); - if (inFluid) { - this.kai.setOrigin(0.5, 0.8); - this.kai.setTint(0xdddddd); + if (inWater) { + this.kai.setOrigin(0.5, 0.75); // Sunken + this.kai.setTint(0xaaccff); } else { this.kai.setOrigin(0.5, 0.9); this.kai.clearTint();