Implement manual player movement planning (steps) and hopping animation
- GameEngine: Added path planning logic (click to add step, re-click to undo). - GameRenderer: Added path visualization (numbered yellow squares). - GameRenderer: Updated animation to include 'hopping' effect and clear path markers on visit. - UIManager: Replaced alerts with modals. - Main: Wired right-click to execute movement.
This commit is contained in:
@@ -240,32 +240,47 @@ export class GameRenderer {
|
||||
}
|
||||
|
||||
if (data.isMoving) {
|
||||
const duration = 400; // ms per tile
|
||||
const duration = 300; // Faster jump (300ms)
|
||||
const elapsed = time - data.startTime;
|
||||
const t = Math.min(elapsed / duration, 1);
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// Lerp X/Z
|
||||
mesh.position.x = THREE.MathUtils.lerp(data.startPos.x, data.targetPos.x, t);
|
||||
mesh.position.z = THREE.MathUtils.lerp(data.startPos.z, data.targetPos.z, t);
|
||||
mesh.position.x = THREE.MathUtils.lerp(data.startPos.x, data.targetPos.x, progress);
|
||||
mesh.position.z = THREE.MathUtils.lerp(data.startPos.z, data.targetPos.z, progress);
|
||||
|
||||
// Jump Arc
|
||||
// Hop (Botecito)
|
||||
const jumpHeight = 0.5;
|
||||
const baseHeight = 1.56 / 2;
|
||||
mesh.position.y = baseHeight + (0.5 * Math.sin(t * Math.PI));
|
||||
mesh.position.y = baseHeight + Math.sin(progress * Math.PI) * jumpHeight;
|
||||
|
||||
if (t >= 1) {
|
||||
mesh.position.set(data.targetPos.x, baseHeight, data.targetPos.z);
|
||||
if (progress >= 1) {
|
||||
data.isMoving = false;
|
||||
mesh.position.y = baseHeight; // Reset height
|
||||
|
||||
// IF Finished Sequence (Queue empty)
|
||||
if (data.pathQueue.length === 0) {
|
||||
// Check if it's the player (id 'p1')
|
||||
if (id === 'p1' && this.onHeroFinishedMove) {
|
||||
// Grid Coords from World Coords (X, -Z)
|
||||
this.onHeroFinishedMove(mesh.position.x, -mesh.position.z);
|
||||
// Remove the visualization tile for this step
|
||||
if (this.pathGroup) {
|
||||
for (let i = this.pathGroup.children.length - 1; i >= 0; i--) {
|
||||
const child = this.pathGroup.children[i];
|
||||
// Match X and Z (ignoring small float errors)
|
||||
if (Math.abs(child.position.x - data.targetPos.x) < 0.1 &&
|
||||
Math.abs(child.position.z - data.targetPos.z) < 0.1) {
|
||||
this.pathGroup.remove(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// IF Finished Sequence (Queue empty)
|
||||
if (data.pathQueue.length === 0) {
|
||||
// Check if it's the player (id 'p1')
|
||||
if (id === 'p1' && this.onHeroFinishedMove) {
|
||||
// Grid Coords from World Coords (X, -Z)
|
||||
this.onHeroFinishedMove(mesh.position.x, -mesh.position.z);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
renderExits(exits) {
|
||||
@@ -585,6 +600,65 @@ export class GameRenderer {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========== PATH VISUALIZATION ==========
|
||||
|
||||
updatePathVisualization(path) {
|
||||
if (!this.pathGroup) {
|
||||
this.pathGroup = new THREE.Group();
|
||||
this.scene.add(this.pathGroup);
|
||||
}
|
||||
|
||||
this.pathGroup.clear();
|
||||
|
||||
if (!path || path.length === 0) return;
|
||||
|
||||
path.forEach((step, index) => {
|
||||
const geometry = new THREE.PlaneGeometry(0.8, 0.8);
|
||||
const texture = this.createNumberTexture(index + 1);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
opacity: 0.8, // Texture itself has opacity
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
const plane = new THREE.Mesh(geometry, material);
|
||||
plane.position.set(step.x, 0.02, -step.y); // Slightly above floor
|
||||
plane.rotation.x = -Math.PI / 2;
|
||||
|
||||
// Store step index to identify it later if needed
|
||||
plane.userData.stepIndex = index;
|
||||
|
||||
this.pathGroup.add(plane);
|
||||
});
|
||||
}
|
||||
|
||||
createNumberTexture(number) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Yellow background with 50% opacity
|
||||
ctx.fillStyle = 'rgba(255, 255, 0, 0.5)';
|
||||
ctx.fillRect(0, 0, 64, 64);
|
||||
|
||||
// Border
|
||||
ctx.strokeStyle = '#EDA900';
|
||||
ctx.lineWidth = 4;
|
||||
ctx.strokeRect(0, 0, 64, 64);
|
||||
|
||||
// Text
|
||||
ctx.font = 'bold 36px Arial';
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(number.toString(), 32, 32);
|
||||
|
||||
const tex = new THREE.CanvasTexture(canvas);
|
||||
// tex.magFilter = THREE.NearestFilter; // Optional, might look pixelated
|
||||
return tex;
|
||||
}
|
||||
|
||||
isPlayerAdjacentToDoor(playerX, playerY, doorMesh) {
|
||||
if (!doorMesh || !doorMesh.userData.isDoor) return false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user