Compare commits
2 Commits
ff6b6bfb73
...
9832d3d6b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9832d3d6b9 | ||
|
|
7aaadee3c5 |
@@ -112,7 +112,24 @@ export const GAME_CONFIG = {
|
||||
fogAlphaWall: 0.35,
|
||||
visibleMinAlpha: 0.35,
|
||||
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: {
|
||||
|
||||
62
src/engine/systems/MineCartSystem.ts
Normal file
62
src/engine/systems/MineCartSystem.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
45
src/engine/systems/TrackSystem.ts
Normal file
45
src/engine/systems/TrackSystem.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { GameScene } from "./scenes/GameScene";
|
||||
import { MenuScene } from "./scenes/MenuScene";
|
||||
import { PreloadScene } from "./scenes/PreloadScene";
|
||||
import { AssetViewerScene } from "./scenes/AssetViewerScene";
|
||||
import { TrackExplorationScene } from "./scenes/TrackExplorationScene";
|
||||
|
||||
new Phaser.Game({
|
||||
type: Phaser.AUTO,
|
||||
@@ -19,5 +20,5 @@ new Phaser.Game({
|
||||
dom: {
|
||||
createContainer: true
|
||||
},
|
||||
scene: [PreloadScene, MenuScene, AssetViewerScene, GameScene, GameUI]
|
||||
scene: [PreloadScene, MenuScene, AssetViewerScene, TrackExplorationScene, GameScene, GameUI]
|
||||
});
|
||||
|
||||
@@ -58,6 +58,7 @@ export class MenuScene extends Phaser.Scene {
|
||||
const startBtn = this.createButton(width / 2, buttonYStart, "ENTER DUNGEON", 0x2288ff);
|
||||
const optBtn = this.createButton(width / 2, buttonYStart + 80, "OPTIONS", 0x444444);
|
||||
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", () => {
|
||||
this.cameras.main.fadeOut(1000, 0, 0, 0);
|
||||
@@ -76,6 +77,13 @@ export class MenuScene extends Phaser.Scene {
|
||||
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() {
|
||||
|
||||
218
src/scenes/TrackExplorationScene.ts
Normal file
218
src/scenes/TrackExplorationScene.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
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 trackSprites: Phaser.GameObjects.Sprite[] = [];
|
||||
private cart?: Phaser.GameObjects.Sprite;
|
||||
private cartPos = { x: 0, y: 0 };
|
||||
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");
|
||||
});
|
||||
|
||||
backBtn.setAlpha(0.8);
|
||||
|
||||
const regenBtn = this.add.text(width / 2, height - 70, "REGENERATE LOOP", {
|
||||
fontSize: "24px",
|
||||
color: "#00ff88"
|
||||
}).setOrigin(0.5)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.on("pointerdown", () => {
|
||||
this.generateRandomLoop();
|
||||
});
|
||||
|
||||
regenBtn.setAlpha(0.8);
|
||||
|
||||
// Initial generation
|
||||
this.generateRandomLoop();
|
||||
|
||||
// Add Mine Cart
|
||||
const { mineCarts } = GAME_CONFIG.rendering;
|
||||
this.cart = this.add.sprite(0, 0, "kennys_dungeon", mineCarts.horizontal);
|
||||
this.cart.setScale(2);
|
||||
this.cart.setDepth(10);
|
||||
this.resetCart();
|
||||
|
||||
// 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 generateRandomLoop() {
|
||||
// Clear existing
|
||||
this.trackSprites.forEach(s => s.destroy());
|
||||
this.trackSprites = [];
|
||||
this.tracks.clear();
|
||||
|
||||
// Start with a 6x6 rectangle
|
||||
const startX = 6;
|
||||
const startY = 6;
|
||||
const initialSize = 15;
|
||||
|
||||
for (let x = startX; x < startX + initialSize; x++) {
|
||||
this.tracks.add(`${x},${startY}`);
|
||||
this.tracks.add(`${x},${startY + initialSize - 1}`);
|
||||
}
|
||||
for (let y = startY + 1; y < startY + initialSize - 1; y++) {
|
||||
this.tracks.add(`${startX},${y}`);
|
||||
this.tracks.add(`${startX + initialSize - 1},${y}`);
|
||||
}
|
||||
|
||||
// Apply "perturbations" to create curves
|
||||
for (let i = 0; i < 100; i++) {
|
||||
this.expandLoop();
|
||||
}
|
||||
|
||||
this.renderTracks();
|
||||
this.resetCart();
|
||||
}
|
||||
|
||||
private expandLoop() {
|
||||
const trackArray = Array.from(this.tracks);
|
||||
const posKey = trackArray[Math.floor(Math.random() * trackArray.length)];
|
||||
const [x, y] = posKey.split(',').map(Number);
|
||||
|
||||
const directions = [
|
||||
{ dx: 1, dy: 0 }, { dx: -1, dy: 0 },
|
||||
{ dx: 0, dy: 1 }, { dx: 0, dy: -1 }
|
||||
];
|
||||
|
||||
for (const dir of directions) {
|
||||
const isH = this.tracks.has(`${x-1},${y}`) && this.tracks.has(`${x+1},${y}`);
|
||||
const isV = this.tracks.has(`${x},${y-1}`) && this.tracks.has(`${x},${y+1}`);
|
||||
|
||||
if (isH && !isV && Math.abs(dir.dy) === 1) {
|
||||
const ny = y + dir.dy;
|
||||
const newTiles = [`${x-1},${ny}`, `${x},${ny}`, `${x+1},${ny}`];
|
||||
|
||||
if (newTiles.some(k => this.tracks.has(k))) continue;
|
||||
|
||||
const tempTracks = new Set(this.tracks);
|
||||
tempTracks.delete(`${x},${y}`);
|
||||
newTiles.forEach(k => tempTracks.add(k));
|
||||
|
||||
// Check rule: Every modified/new tile must have exactly 2 neighbors
|
||||
const affected = [...newTiles, `${x-1},${y}`, `${x+1},${y}`];
|
||||
const isValid = affected.every(posKey => {
|
||||
const [px, py] = posKey.split(',').map(Number);
|
||||
const count = [[px+1,py],[px-1,py],[px,py+1],[px,py-1]]
|
||||
.filter(([nx, ny]) => tempTracks.has(`${nx},${ny}`)).length;
|
||||
return count === 2;
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
this.tracks = tempTracks;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isV && !isH && Math.abs(dir.dx) === 1) {
|
||||
const nx = x + dir.dx;
|
||||
const newTiles = [`${nx},${y-1}`, `${nx},${y}`, `${nx},${y+1}`];
|
||||
|
||||
if (newTiles.some(k => this.tracks.has(k))) continue;
|
||||
|
||||
const tempTracks = new Set(this.tracks);
|
||||
tempTracks.delete(`${x},${y}`);
|
||||
newTiles.forEach(k => tempTracks.add(k));
|
||||
|
||||
const affected = [...newTiles, `${x},${y-1}`, `${x},${y+1}`];
|
||||
const isValid = affected.every(posKey => {
|
||||
const [px, py] = posKey.split(',').map(Number);
|
||||
const count = [[px+1,py],[px-1,py],[px,py+1],[px,py-1]]
|
||||
.filter(([nx, ny]) => tempTracks.has(`${nx},${ny}`)).length;
|
||||
return count === 2;
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
this.tracks = tempTracks;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private resetCart() {
|
||||
if (!this.cart) return;
|
||||
const first = Array.from(this.tracks)[0];
|
||||
const [x, y] = first.split(',').map(Number);
|
||||
this.cartPos = { x, y };
|
||||
this.cart.setPosition(x * 32 + 16, y * 32 + 16);
|
||||
|
||||
// Find a valid initial direction
|
||||
const neighbors = [
|
||||
{ dx: 1, dy: 0 }, { dx: -1, dy: 0 },
|
||||
{ dx: 0, dy: 1 }, { dx: 0, dy: -1 }
|
||||
];
|
||||
for (const n of neighbors) {
|
||||
if (this.tracks.has(`${x + n.dx},${y + n.dy}`)) {
|
||||
this.cartDir = { dx: n.dx, dy: n.dy };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const sprite = this.add.sprite(x * 32 + 16, y * 32 + 16, "kennys_dungeon", frame).setScale(2);
|
||||
this.trackSprites.push(sprite);
|
||||
});
|
||||
}
|
||||
|
||||
private moveCart() {
|
||||
if (!this.cart || this.tracks.size === 0) 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user