Fix: Water Trench Effect & Layering (Subtract Mask) - Implemented RenderTexture erasing for true depth, cleaned up Z-sorting, and optimized GrassScene_Clean.
This commit is contained in:
BIN
assets/environment/potok_segment.png
Normal file
BIN
assets/environment/potok_segment.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
31
scripts/crop_stream.py
Normal file
31
scripts/crop_stream.py
Normal file
@@ -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()
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user