feat: Add scene with track loop and mine cart

This commit is contained in:
Peter Stockings
2026-01-21 20:18:02 +11:00
parent ff6b6bfb73
commit 7aaadee3c5
6 changed files with 239 additions and 2 deletions

View File

@@ -112,7 +112,24 @@ export const GAME_CONFIG = {
fogAlphaWall: 0.35, fogAlphaWall: 0.35,
visibleMinAlpha: 0.35, visibleMinAlpha: 0.35,
visibleMaxAlpha: 1.0, visibleMaxAlpha: 1.0,
visibleStrengthFactor: 0.65 visibleStrengthFactor: 0.65,
tracks: {
endTop: 67,
endBottom: 68,
cornerNE: 93,
horizontal: 70,
cornerSE: 69,
endLeft: 79,
endRight: 80,
vertical: 81,
cornerSW: 71,
cornerNW: 95
},
mineCarts: {
horizontal: 54,
vertical: 55,
turning: 56
}
}, },
ui: { ui: {

View File

@@ -0,0 +1,62 @@
import Phaser from "phaser";
import { GAME_CONFIG } from "../../core/config/GameConfig";
import { TrackDirection } from "./TrackSystem";
export interface MineCartState {
x: number;
y: number;
facing: { dx: number, dy: number };
}
export class MineCartSystem {
static updateOrientation(sprite: Phaser.GameObjects.Sprite, dx: number, dy: number, _connections: TrackDirection) {
const { mineCarts } = GAME_CONFIG.rendering;
// Horizontal movement
if (dx !== 0 && dy === 0) {
sprite.setFrame(mineCarts.horizontal);
sprite.setFlipX(dx < 0);
sprite.setAngle(0);
}
// Vertical movement
else if (dy !== 0 && dx === 0) {
sprite.setFrame(mineCarts.vertical);
sprite.setFlipY(false);
sprite.setAngle(0);
}
// Turning (Corner case)
else {
sprite.setFrame(mineCarts.turning);
// Logic for 56 (turned from right to down by default)
// We need to rotate/flip to match the actual turn.
// This is a bit complex without seeing the sprite, but we'll approximate:
if (dx > 0 && dy > 0) sprite.setAngle(0); // Right to Down
if (dx < 0 && dy < 0) sprite.setAngle(180); // Left to Up
if (dx > 0 && dy < 0) sprite.setAngle(-90); // Right to Up
if (dx < 0 && dy > 0) sprite.setAngle(90); // Left to Down
}
}
static getNextPosition(current: { x: number, y: number }, dx: number, dy: number, isTrack: (x: number, y: number) => boolean): { x: number, y: number, dx: number, dy: number } | null {
const nextX = current.x + dx;
const nextY = current.y + dy;
if (isTrack(nextX, nextY)) {
return { x: nextX, y: nextY, dx, dy };
}
// Try turning if blocked
const possibleTurns = [
{ tdx: dy, tdy: -dx }, // Left turn
{ tdx: -dy, tdy: dx } // Right turn
];
for (const turn of possibleTurns) {
if (isTrack(current.x + turn.tdx, current.y + turn.tdy)) {
return { x: current.x + turn.tdx, y: current.y + turn.tdy, dx: turn.tdx, dy: turn.tdy };
}
}
return null;
}
}

View File

@@ -0,0 +1,45 @@
import { GAME_CONFIG } from "../../core/config/GameConfig";
export const TrackDirection = {
NONE: 0,
NORTH: 1 << 0,
SOUTH: 1 << 1,
EAST: 1 << 2,
WEST: 1 << 3
} as const;
export type TrackDirection = number;
export class TrackSystem {
static getTrackFrame(connections: TrackDirection): number {
const { tracks } = GAME_CONFIG.rendering;
// Dead Ends
if (connections === TrackDirection.SOUTH) return tracks.endTop;
if (connections === TrackDirection.NORTH) return tracks.endBottom;
if (connections === TrackDirection.EAST) return tracks.endLeft;
if (connections === TrackDirection.WEST) return tracks.endRight;
// Straights
if (connections === (TrackDirection.NORTH | TrackDirection.SOUTH)) return tracks.vertical;
if (connections === (TrackDirection.EAST | TrackDirection.WEST)) return tracks.horizontal;
// Corners
if (connections === (TrackDirection.NORTH | TrackDirection.EAST)) return tracks.cornerNE;
if (connections === (TrackDirection.SOUTH | TrackDirection.EAST)) return tracks.cornerSE;
if (connections === (TrackDirection.SOUTH | TrackDirection.WEST)) return tracks.cornerSW;
if (connections === (TrackDirection.NORTH | TrackDirection.WEST)) return tracks.cornerNW;
// Fallback to horizontal
return tracks.horizontal;
}
static getConnectionsFromNeighbors(x: number, y: number, isTrack: (x: number, y: number) => boolean): TrackDirection {
let connections = TrackDirection.NONE;
if (isTrack(x, y - 1)) connections |= TrackDirection.NORTH;
if (isTrack(x, y + 1)) connections |= TrackDirection.SOUTH;
if (isTrack(x + 1, y)) connections |= TrackDirection.EAST;
if (isTrack(x - 1, y)) connections |= TrackDirection.WEST;
return connections;
}
}

View File

@@ -4,6 +4,7 @@ import { GameScene } from "./scenes/GameScene";
import { MenuScene } from "./scenes/MenuScene"; import { MenuScene } from "./scenes/MenuScene";
import { PreloadScene } from "./scenes/PreloadScene"; import { PreloadScene } from "./scenes/PreloadScene";
import { AssetViewerScene } from "./scenes/AssetViewerScene"; import { AssetViewerScene } from "./scenes/AssetViewerScene";
import { TrackExplorationScene } from "./scenes/TrackExplorationScene";
new Phaser.Game({ new Phaser.Game({
type: Phaser.AUTO, type: Phaser.AUTO,
@@ -19,5 +20,5 @@ new Phaser.Game({
dom: { dom: {
createContainer: true createContainer: true
}, },
scene: [PreloadScene, MenuScene, AssetViewerScene, GameScene, GameUI] scene: [PreloadScene, MenuScene, AssetViewerScene, TrackExplorationScene, GameScene, GameUI]
}); });

View File

@@ -58,6 +58,7 @@ export class MenuScene extends Phaser.Scene {
const startBtn = this.createButton(width / 2, buttonYStart, "ENTER DUNGEON", 0x2288ff); const startBtn = this.createButton(width / 2, buttonYStart, "ENTER DUNGEON", 0x2288ff);
const optBtn = this.createButton(width / 2, buttonYStart + 80, "OPTIONS", 0x444444); const optBtn = this.createButton(width / 2, buttonYStart + 80, "OPTIONS", 0x444444);
const assetViewerBtn = this.createButton(width / 2, buttonYStart + 160, "ASSET VIEWER", 0xff9922); const assetViewerBtn = this.createButton(width / 2, buttonYStart + 160, "ASSET VIEWER", 0xff9922);
const trackExplorationBtn = this.createButton(width / 2, buttonYStart + 240, "TRACK EXPLORATION", 0x00ff88);
startBtn.on("pointerdown", () => { startBtn.on("pointerdown", () => {
this.cameras.main.fadeOut(1000, 0, 0, 0); this.cameras.main.fadeOut(1000, 0, 0, 0);
@@ -76,6 +77,13 @@ export class MenuScene extends Phaser.Scene {
this.scene.start("AssetViewerScene"); this.scene.start("AssetViewerScene");
}); });
}); });
trackExplorationBtn.on("pointerdown", () => {
this.cameras.main.fadeOut(500, 0, 0, 0);
this.cameras.main.once(Phaser.Cameras.Scene2D.Events.FADE_OUT_COMPLETE, () => {
this.scene.start("TrackExplorationScene");
});
});
} }
private createWindEffect() { private createWindEffect() {

View File

@@ -0,0 +1,104 @@
import { GAME_CONFIG } from "../core/config/GameConfig";
import { TrackSystem } from "../engine/systems/TrackSystem";
import { MineCartSystem } from "../engine/systems/MineCartSystem";
export class TrackExplorationScene extends Phaser.Scene {
constructor() {
super("TrackExplorationScene");
}
private tracks: Set<string> = new Set();
private cart?: Phaser.GameObjects.Sprite;
private cartPos = { x: 5, y: 5 };
private cartDir = { dx: 1, dy: 0 };
private moveTimer?: Phaser.Time.TimerEvent;
create() {
const { width, height } = this.scale;
this.add.text(width / 2, 30, "TRACK EXPLORATION", {
fontSize: "32px",
color: "#ffffff"
}).setOrigin(0.5);
const backBtn = this.add.text(width / 2, height - 30, "BACK TO MENU", {
fontSize: "24px",
color: "#2288ff"
}).setOrigin(0.5)
.setInteractive({ useHandCursor: true })
.on("pointerdown", () => {
this.scene.start("MenuScene");
});
// Silence lint warning if needed
backBtn.setAlpha(0.8);
// Create a smaller track loop to test all corners clearly
const startX = 5;
const startY = 5;
const size = 5; // 5x5 loop
for (let x = startX; x < startX + size; x++) {
this.tracks.add(`${x},${startY}`);
this.tracks.add(`${x},${startY + size - 1}`);
}
for (let y = startY + 1; y < startY + size - 1; y++) {
this.tracks.add(`${startX},${y}`);
this.tracks.add(`${startX + size - 1},${y}`);
}
// Render tracks
this.renderTracks();
// Add Mine Cart
const { mineCarts } = GAME_CONFIG.rendering;
this.cart = this.add.sprite(5 * 32 + 16, 5 * 32 + 16, "kennys_dungeon", mineCarts.horizontal);
this.cart.setScale(2); // Make it visible
// Movement Loop
this.moveTimer = this.time.addEvent({
delay: 500,
callback: this.moveCart,
callbackScope: this,
loop: true
});
this.events.on('shutdown', () => {
if (this.moveTimer) this.moveTimer.destroy();
});
}
private renderTracks() {
this.tracks.forEach(posKey => {
const [x, y] = posKey.split(',').map(Number);
const connections = TrackSystem.getConnectionsFromNeighbors(x, y, (nx: number, ny: number) => this.tracks.has(`${nx},${ny}`));
const frame = TrackSystem.getTrackFrame(connections);
this.add.sprite(x * 32 + 16, y * 32 + 16, "kennys_dungeon", frame).setScale(2);
});
}
private moveCart() {
if (!this.cart) return;
const next = MineCartSystem.getNextPosition(this.cartPos, this.cartDir.dx, this.cartDir.dy, (nx: number, ny: number) => this.tracks.has(`${nx},${ny}`));
if (next) {
this.cartPos = { x: next.x, y: next.y };
this.cartDir = { dx: next.dx, dy: next.dy };
const tx = next.x * 32 + 16;
const ty = next.y * 32 + 16;
this.tweens.add({
targets: this.cart,
x: tx,
y: ty,
duration: 400,
ease: 'Linear',
onStart: () => {
const connections = TrackSystem.getConnectionsFromNeighbors(next.x, next.y, (nx: number, ny: number) => this.tracks.has(`${nx},${ny}`));
MineCartSystem.updateOrientation(this.cart!, next.dx, next.dy, connections);
}
});
}
}
}