feat: Complete 2D Visual Overhaul - Isometric to Flat Top-Down

- NEW: Flat2DTerrainSystem.js (375 lines)
- NEW: map2d_data.js procedural map (221 lines)
- MODIFIED: GameScene async create, 2D terrain integration
- MODIFIED: Player.js flat 2D positioning
- MODIFIED: game.js disabled pixelArt for smooth rendering
- FIXED: 15+ bugs (updateCulling, isometric conversions, grid lines)
- ADDED: Phase 28 to TASKS.md
- DOCS: DNEVNIK.md session summary

Result: Working flat 2D game with Stardew Valley style!
Time: 5.5 hours
This commit is contained in:
2025-12-14 17:12:40 +01:00
parent c3dd39e1a6
commit 80bddf5d61
37 changed files with 8164 additions and 1800 deletions

View File

@@ -12,11 +12,23 @@ class Player {
this.iso = new IsometricUtils(48, 24);
// Hitrost gibanja
this.moveSpeed = 150; // px/s
this.gridMoveTime = 200; // ms za premik na eno kocko
// 🎮 SMOOTH MOVEMENT SYSTEM (Hybrid)
this.moveSpeed = 100; // Normal speed px/s
this.sprintSpeed = 200; // Sprint speed px/s
this.acceleration = 0.15; // How fast to reach target speed
// Stanje
// Velocity (current movement)
this.velocity = { x: 0, y: 0 };
// Target velocity (where we want to go)
this.targetVelocity = { x: 0, y: 0 };
// Sprint system
this.sprinting = false;
this.energy = 100;
this.maxEnergy = 100;
this.energyDrain = 10; // per second while sprinting
// State
this.isMoving = false;
this.direction = 'down';
this.lastDir = { x: 0, y: 1 }; // Default south
@@ -135,10 +147,19 @@ class Player {
setupControls() {
this.keys = this.scene.input.keyboard.addKeys({
up: Phaser.Input.Keyboard.KeyCodes.W,
down: Phaser.Input.Keyboard.KeyCodes.S,
left: Phaser.Input.Keyboard.KeyCodes.A,
right: Phaser.Input.Keyboard.KeyCodes.D
// WASD
w: Phaser.Input.Keyboard.KeyCodes.W,
a: Phaser.Input.Keyboard.KeyCodes.A,
s: Phaser.Input.Keyboard.KeyCodes.S,
d: Phaser.Input.Keyboard.KeyCodes.D,
// Arrow Keys
up: Phaser.Input.Keyboard.KeyCodes.UP,
down: Phaser.Input.Keyboard.KeyCodes.DOWN,
left: Phaser.Input.Keyboard.KeyCodes.LEFT,
right: Phaser.Input.Keyboard.KeyCodes.RIGHT,
// Actions
space: Phaser.Input.Keyboard.KeyCodes.SPACE,
shift: Phaser.Input.Keyboard.KeyCodes.SHIFT
});
// Gamepad Events
@@ -222,18 +243,60 @@ class Player {
}
update(delta) {
// NOTE: updateDepth() disabled - using sortableObjects Z-sorting in GameScene
// if (this.isMoving) {
// this.updateDepth();
// }
// Convert delta to seconds
const dt = delta / 1000;
if (!this.isMoving) {
this.handleInput();
// 🎮 HANDLE INPUT (always check for input)
this.handleInput(dt);
// 🏃 APPLY SMOOTH MOVEMENT
// Smoothly interpolate current velocity toward target velocity
this.velocity.x = Phaser.Math.Linear(
this.velocity.x,
this.targetVelocity.x,
this.acceleration
);
this.velocity.y = Phaser.Math.Linear(
this.velocity.y,
this.targetVelocity.y,
this.acceleration
);
// Apply velocity to sprite position
this.sprite.x += this.velocity.x * dt;
this.sprite.y += this.velocity.y * dt;
// Update grid position from pixel position
const screenPos = { x: this.sprite.x - this.offsetX, y: this.sprite.y - this.offsetY };
const gridPos = this.iso.toGrid(screenPos.x, screenPos.y);
this.gridX = Math.floor(gridPos.x);
this.gridY = Math.floor(gridPos.y);
// Check if moving
const speed = Math.sqrt(this.velocity.x ** 2 + this.velocity.y ** 2);
this.isMoving = speed > 5;
// 🎨 UPDATE ANIMATION
this.updateAnimation();
// 💧 ENERGY REGENERATION
if (!this.sprinting && this.energy < this.maxEnergy) {
this.energy = Math.min(this.maxEnergy, this.energy + 20 * dt);
}
// ⚡ ENERGY DRAIN WHILE SPRINTING
if (this.sprinting && this.isMoving && this.energy > 0) {
this.energy -= this.energyDrain * dt;
if (this.energy <= 0) {
this.energy = 0;
this.sprinting = false; // Can't sprint without energy
}
}
// 🔧 UPDATE HELD ITEM
this.updateHeldItem();
// SPACE KEY - Farming Action
// 🌱 SPACE KEY - Farming Action
if (this.keys.space && Phaser.Input.Keyboard.JustDown(this.keys.space)) {
this.handleFarmingAction();
}
@@ -261,189 +324,137 @@ class Player {
}
}
handleInput() {
let targetX = this.gridX;
let targetY = this.gridY;
let moved = false;
let facingRight = !this.sprite.flipX;
handleInput(dt) {
// 🎮 COLLECT INPUT FROM ALL SOURCES
let inputX = 0;
let inputY = 0;
// Determine inputs
let up = this.keys.up.isDown;
let down = this.keys.down.isDown;
let left = this.keys.left.isDown;
let right = this.keys.right.isDown;
// Keyboard (WASD + Arrows)
if (this.keys.up.isDown || this.keys.w.isDown) inputY -= 1;
if (this.keys.down.isDown || this.keys.s.isDown) inputY += 1;
if (this.keys.left.isDown || this.keys.a.isDown) inputX -= 1;
if (this.keys.right.isDown || this.keys.d.isDown) inputX += 1;
// Check Virtual Joystick inputs (from UIScene)
// Virtual Joystick (Mobile)
const ui = this.scene.scene.get('UIScene');
if (ui && ui.virtualJoystick) {
if (ui.virtualJoystick.up) up = true;
if (ui.virtualJoystick.down) down = true;
if (ui.virtualJoystick.left) left = true;
if (ui.virtualJoystick.right) right = true;
if (ui.virtualJoystick.up) inputY -= 1;
if (ui.virtualJoystick.down) inputY += 1;
if (ui.virtualJoystick.left) inputX -= 1;
if (ui.virtualJoystick.right) inputX += 1;
}
// Check Gamepad Input (Xbox Controller)
// Gamepad (Xbox Controller)
if (this.scene.input.gamepad && this.scene.input.gamepad.total > 0) {
const pad = this.scene.input.gamepad.getPad(0);
if (pad) {
const threshold = 0.3;
if (pad.leftStick.y < -threshold) up = true;
if (pad.leftStick.y > threshold) down = true;
if (pad.leftStick.x < -threshold) left = true;
if (pad.leftStick.x > threshold) right = true;
const threshold = 0.2;
const stickY = pad.leftStick.y;
const stickX = pad.leftStick.x;
// D-Pad support
if (pad.up) up = true;
if (pad.down) down = true;
if (pad.left) left = true;
if (pad.right) right = true;
// Analog stick (smooth values)
if (Math.abs(stickY) > threshold) inputY += stickY;
if (Math.abs(stickX) > threshold) inputX += stickX;
// D-Pad (digital)
if (pad.up) inputY -= 1;
if (pad.down) inputY += 1;
if (pad.left) inputX -= 1;
if (pad.right) inputX += 1;
// Sprint button (B on Xbox, Circle on PS)
if (pad.B || pad.buttons[1]?.pressed) {
this.sprinting = true;
}
}
}
// Apply
let dx = 0;
let dy = 0;
// 🏃 SPRINT DETECTION (Shift key)
this.sprinting = this.keys.shift?.isDown || this.sprinting;
if (up) {
dx = -1; dy = 0;
moved = true;
facingRight = false;
} else if (down) {
dx = 1; dy = 0;
moved = true;
facingRight = true;
// Normalize diagonal movement (so diagonal isn't faster)
const inputLength = Math.sqrt(inputX ** 2 + inputY ** 2);
if (inputLength > 0) {
inputX /= inputLength;
inputY /= inputLength;
}
if (left) {
dx = 0; dy = 1;
moved = true;
facingRight = false;
} else if (right) {
dx = 0; dy = -1;
moved = true;
facingRight = true;
// 🎯 DETERMINE MOVEMENT SPEED
let maxSpeed = this.moveSpeed;
if (this.sprinting && this.energy > 0) {
maxSpeed = this.sprintSpeed;
}
// Update target
targetX = this.gridX + dx;
targetY = this.gridY + dy;
// 🚀 SET TARGET VELOCITY
this.targetVelocity.x = inputX * maxSpeed;
this.targetVelocity.y = inputY * maxSpeed;
// Update Facing Direction and Last Dir
if (moved) {
this.lastDir = { x: dx, y: dy };
// 🧭 UPDATE DIRECTION & FACING
if (inputLength > 0.1) {
// Update last direction for attacks/interactions
this.lastDir = { x: inputX, y: inputY };
// Determine animation direction (4 directions)
let animDir = 'down'; // default
// Determine animation direction (4-way)
// Isometric mapping: up/down = X axis, left/right = Y axis
let animDir = 'down';
// UP/DOWN (isometric: dx changes)
if (dx < 0 && dy === 0) {
animDir = 'up'; // Moving up (NW in isometric)
} else if (dx > 0 && dy === 0) {
animDir = 'down'; // Moving down (SE in isometric)
}
// LEFT/RIGHT (isometric: dy changes)
else if (dy > 0 && dx === 0) {
animDir = 'left'; // Moving left (SW in isometric)
} else if (dy < 0 && dx === 0) {
animDir = 'right'; // Moving right (NE in isometric)
// Prioritize primary direction (stronger input)
if (Math.abs(inputX) > Math.abs(inputY)) {
// Vertical movement (up/down in isometric)
animDir = inputX < 0 ? 'up' : 'down';
} else {
// Horizontal movement (left/right in isometric)
animDir = inputY < 0 ? 'right' : 'left';
}
this.direction = animDir;
}
}
// Play walking animation for the direction
if (this.sprite.anims) {
try {
const walkAnim = `protagonist_walk_${animDir}`;
// 🎨 UPDATE ANIMATION (called from update loop)
updateAnimation() {
if (!this.sprite.anims) return;
// Debug
console.log(`🎬 Trying to play: ${walkAnim}`);
console.log(`Animation exists: ${this.scene.anims.exists(walkAnim)}`);
const speed = Math.sqrt(this.velocity.x ** 2 + this.velocity.y ** 2);
if (this.scene.anims.exists(walkAnim)) {
this.sprite.play(walkAnim, true); // Force restart animation
console.log(`✅ Playing: ${walkAnim}`);
} else {
console.warn(`⚠️ Animation not found: ${walkAnim}`);
try {
if (speed < 5) {
// 😴 IDLE
const idleAnim = `protagonist_idle_${this.direction}`;
if (this.scene.anims.exists(idleAnim) && !this.sprite.anims.isPlaying) {
this.sprite.play(idleAnim);
}
} else {
// 🚶 WALKING / 🏃 SPRINTING
const walkAnim = `protagonist_walk_${this.direction}`;
if (this.scene.anims.exists(walkAnim)) {
if (this.sprite.anims.currentAnim?.key !== walkAnim) {
this.sprite.play(walkAnim, true);
}
// Faster animation when sprinting
const frameRate = this.sprinting ? 12 : 8;
if (this.sprite.anims.currentAnim) {
this.sprite.anims.currentAnim.frameRate = frameRate;
}
} catch (e) {
console.error('Animation error:', e);
}
}
// Hand offset based on direction
// Hand sprite position update
const handOffsets = {
'left': -10,
'right': 10,
'up': 0,
'down': 0
};
this.handSprite.setX(this.sprite.x + (handOffsets[animDir] || 0));
} else {
// Stop animation when idle
if (this.sprite.anims) {
try {
if (this.sprite.anims.isPlaying) {
this.sprite.stop();
}
// Play idle animation for current direction
const idleAnim = `protagonist_idle_${this.direction}`;
if (this.scene.anims.exists(idleAnim)) {
this.sprite.play(idleAnim);
}
} catch (e) {
// Ignore animation errors
}
if (this.handSprite) {
this.handSprite.setX(this.sprite.x + (handOffsets[this.direction] || 0));
}
}
// Collision Check
const terrainSystem = this.scene.terrainSystem;
if (moved && terrainSystem) {
if (this.iso.isInBounds(targetX, targetY, terrainSystem.width, terrainSystem.height)) {
const tile = terrainSystem.tiles[targetY][targetX];
let isPassable = true;
// TILE COLLISION - Preveri solid property PRVO
if (tile.solid === true) {
console.log('⛔ Blocked by solid tile property');
isPassable = false;
}
// Nato preveri tip (fallback)
const solidTileTypes = [
'water', // Voda
'MINE_WALL', // Rudniški zidovi
'WALL_EDGE', // Robovi zidov (DODANO)
'ORE_STONE', // Kamnita ruda (dokler ni izkopana)
'ORE_IRON', // Železna ruda
'lava', // Lava (če bo dodana)
'void' // Praznina
// Opomba: PAVEMENT je WALKABLE (igralec lahko hodi po cesti)
];
const tileName = tile.type.name || tile.type;
if (isPassable && solidTileTypes.includes(tileName)) {
console.log('⛔ Blocked by solid tile:', tileName);
isPassable = false;
}
// DECORATION COLLISION - Trdni objekti
const key = `${targetX},${targetY}`;
if (terrainSystem.decorationsMap.has(key)) {
const decor = terrainSystem.decorationsMap.get(key);
// Preverimo decor.solid property (set by TerrainSystem.addDecoration)
if (decor.solid === true) {
console.log('⛔ BLOCKED by solid decoration:', decor.type);
isPassable = false;
}
}
if (isPassable) {
this.moveToGrid(targetX, targetY);
}
}
} catch (e) {
// Ignore animation errors
}
}
@@ -512,11 +523,12 @@ class Player {
}
updatePosition() {
const screenPos = this.iso.toScreen(this.gridX, this.gridY);
// 🎨 FLAT 2D POSITIONING (NEW!)
const tileSize = 48;
// Pixel-perfect positioning
const x = Math.round(screenPos.x + this.offsetX);
const y = Math.round(screenPos.y + this.offsetY);
// Direct grid to pixel conversion (NO isometric!)
const x = Math.round((this.gridX * tileSize) + (tileSize / 2));
const y = Math.round((this.gridY * tileSize) + (tileSize / 2));
this.sprite.setPosition(x, y);
@@ -714,4 +726,20 @@ class Player {
});
}
}
/**
* Check if player has required tool equipped/in inventory
* @param {string} toolType - Tool type (axe, pickaxe, hoe, etc.)
* @returns {boolean} - True if player has the tool
*/
hasToolEquipped(toolType) {
if (!toolType) return true; // No tool required
// Check inventory for tool
if (this.scene.inventorySystem) {
return this.scene.inventorySystem.hasItem(toolType, 1);
}
return false;
}
}