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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user