645 lines
19 KiB
JavaScript
645 lines
19 KiB
JavaScript
/**
|
|
* TransportSystem.js
|
|
* ==================
|
|
* Manages all transportation methods in the game
|
|
* - Horses (5 breeds, different speeds)
|
|
* - Carts & Wagons (cargo capacity)
|
|
* - Trains (track-based, high speed, high capacity)
|
|
* - Water transport (kayak, SUP, raft, boat)
|
|
* - Bicycles & Boards (longboard, mountain board, snowboard)
|
|
*
|
|
* Uses: transportation_vehicles_detailed_1766097668396.tsx
|
|
* train_repairs_3_states (broken → repairing → rideable)
|
|
*
|
|
* @author NovaFarma Team
|
|
* @date 2025-12-22
|
|
*/
|
|
|
|
export default class TransportSystem {
|
|
constructor(scene) {
|
|
this.scene = scene;
|
|
|
|
// Current state
|
|
this.currentVehicle = null;
|
|
this.ownedVehicles = new Map();
|
|
this.trainTracks = [];
|
|
|
|
// Vehicle definitions
|
|
this.vehicleData = this.defineVehicles();
|
|
|
|
// Train repair state
|
|
this.trainRepairProgress = 0;
|
|
|
|
console.log('🚂 TransportSystem initialized');
|
|
}
|
|
|
|
defineVehicles() {
|
|
return {
|
|
// ===== HORSES =====
|
|
horse_basic: {
|
|
name: 'Basic Horse',
|
|
type: 'mount',
|
|
speed: 200,
|
|
capacity: 0,
|
|
cost: 500,
|
|
unlockLevel: 3,
|
|
sprite: 'horse_brown',
|
|
waterAllowed: false,
|
|
description: 'Reliable companion for travel'
|
|
},
|
|
horse_fast: {
|
|
name: 'Racing Horse',
|
|
type: 'mount',
|
|
speed: 300,
|
|
capacity: 0,
|
|
cost: 1500,
|
|
unlockLevel: 7,
|
|
sprite: 'horse_white',
|
|
waterAllowed: false,
|
|
description: 'Swift steed for long distances'
|
|
},
|
|
horse_draft: {
|
|
name: 'Draft Horse',
|
|
type: 'mount',
|
|
speed: 150,
|
|
capacity: 50,
|
|
cost: 1200,
|
|
unlockLevel: 5,
|
|
sprite: 'horse_black',
|
|
waterAllowed: false,
|
|
description: 'Strong horse, can carry cargo'
|
|
},
|
|
|
|
// ===== CARTS & WAGONS =====
|
|
cart_wooden: {
|
|
name: 'Wooden Cart',
|
|
type: 'vehicle',
|
|
speed: 120,
|
|
capacity: 100,
|
|
cost: 800,
|
|
unlockLevel: 4,
|
|
blueprint: 'blueprint_cart',
|
|
sprite: 'cart_wooden',
|
|
waterAllowed: false,
|
|
requiresHorse: true,
|
|
description: 'Transport goods efficiently'
|
|
},
|
|
wagon_large: {
|
|
name: 'Large Wagon',
|
|
type: 'vehicle',
|
|
speed: 100,
|
|
capacity: 250,
|
|
cost: 2500,
|
|
unlockLevel: 8,
|
|
blueprint: 'blueprint_wagon',
|
|
sprite: 'wagon_large',
|
|
waterAllowed: false,
|
|
requiresHorse: true,
|
|
description: 'Massive cargo capacity'
|
|
},
|
|
|
|
// ===== TRAINS =====
|
|
train: {
|
|
name: 'Steam Train',
|
|
type: 'rail',
|
|
speed: 400,
|
|
capacity: 500,
|
|
cost: 10000,
|
|
unlockLevel: 15,
|
|
blueprint: 'blueprint_train',
|
|
sprite: 'train_rideable',
|
|
waterAllowed: false,
|
|
requiresTrack: true,
|
|
repairStages: ['broken', 'repairing', 'rideable'],
|
|
description: 'Ultimate transport - requires tracks'
|
|
},
|
|
|
|
// ===== WATER TRANSPORT =====
|
|
kayak: {
|
|
name: 'Kayak',
|
|
type: 'water',
|
|
speed: 150,
|
|
capacity: 20,
|
|
cost: 300,
|
|
unlockLevel: 6,
|
|
sprite: 'kayak',
|
|
waterOnly: true,
|
|
description: 'Navigate rivers and lakes'
|
|
},
|
|
sup: {
|
|
name: 'Stand-Up Paddleboard',
|
|
type: 'water',
|
|
speed: 100,
|
|
capacity: 5,
|
|
cost: 200,
|
|
unlockLevel: 5,
|
|
sprite: 'sup',
|
|
waterOnly: true,
|
|
description: 'Calm water exploration'
|
|
},
|
|
raft: {
|
|
name: 'Wooden Raft',
|
|
type: 'water',
|
|
speed: 80,
|
|
capacity: 50,
|
|
cost: 150,
|
|
unlockLevel: 3,
|
|
sprite: 'raft',
|
|
waterOnly: true,
|
|
description: 'Basic water transport'
|
|
},
|
|
boat: {
|
|
name: 'Fishing Boat',
|
|
type: 'water',
|
|
speed: 180,
|
|
capacity: 100,
|
|
cost: 1000,
|
|
unlockLevel: 10,
|
|
blueprint: 'blueprint_boat',
|
|
sprite: 'boat',
|
|
waterOnly: true,
|
|
description: 'Ocean-ready vessel'
|
|
},
|
|
|
|
// ===== BICYCLES & BOARDS =====
|
|
bicycle: {
|
|
name: 'Bicycle',
|
|
type: 'manual',
|
|
speed: 180,
|
|
capacity: 10,
|
|
cost: 250,
|
|
unlockLevel: 2,
|
|
sprite: 'bicycle',
|
|
waterAllowed: false,
|
|
description: 'Energy-efficient travel'
|
|
},
|
|
longboard: {
|
|
name: 'Longboard',
|
|
type: 'manual',
|
|
speed: 220,
|
|
capacity: 0,
|
|
cost: 150,
|
|
unlockLevel: 4,
|
|
sprite: 'longboard',
|
|
waterAllowed: false,
|
|
terrainBonus: { 'road': 1.5 }, // 50% faster on roads
|
|
description: 'Cruise downhill paths'
|
|
},
|
|
mountain_board: {
|
|
name: 'Mountain Board',
|
|
type: 'manual',
|
|
speed: 200,
|
|
capacity: 5,
|
|
cost: 300,
|
|
unlockLevel: 6,
|
|
sprite: 'mountain_board',
|
|
waterAllowed: false,
|
|
terrainBonus: { 'mountain': 1.3 },
|
|
description: 'Off-road board for rough terrain'
|
|
},
|
|
snowboard: {
|
|
name: 'Snowboard',
|
|
type: 'manual',
|
|
speed: 250,
|
|
capacity: 0,
|
|
cost: 200,
|
|
unlockLevel: 5,
|
|
sprite: 'snowboard',
|
|
waterAllowed: false,
|
|
terrainRequired: 'snow',
|
|
terrainBonus: { 'snow': 2.0 }, // 2x speed on snow
|
|
description: 'Winter transport - snow only'
|
|
}
|
|
};
|
|
}
|
|
|
|
// ===== VEHICLE MOUNTING =====
|
|
|
|
canMount(vehicleId, playerX, playerY) {
|
|
const vehicle = this.vehicleData[vehicleId];
|
|
if (!vehicle) {
|
|
return { canMount: false, reason: 'Vehicle not found' };
|
|
}
|
|
|
|
// Check if player owns this vehicle
|
|
if (!this.ownedVehicles.has(vehicleId)) {
|
|
return { canMount: false, reason: 'You don\'t own this vehicle' };
|
|
}
|
|
|
|
// Check if already mounted
|
|
if (this.currentVehicle) {
|
|
return { canMount: false, reason: 'Already using a vehicle' };
|
|
}
|
|
|
|
// Check level requirement
|
|
const playerLevel = this.scene.player?.stats?.level || 1;
|
|
if (playerLevel < vehicle.unlockLevel) {
|
|
return {
|
|
canMount: false,
|
|
reason: `Requires level ${vehicle.unlockLevel}`
|
|
};
|
|
}
|
|
|
|
// Check terrain compatibility
|
|
const currentTile = this.getTileAt(playerX, playerY);
|
|
|
|
if (vehicle.waterOnly && !currentTile.properties?.isWater) {
|
|
return { canMount: false, reason: 'Water vehicles require water' };
|
|
}
|
|
|
|
if (!vehicle.waterAllowed && currentTile.properties?.isWater) {
|
|
return { canMount: false, reason: 'Cannot use on water' };
|
|
}
|
|
|
|
if (vehicle.terrainRequired && currentTile.properties?.terrain !== vehicle.terrainRequired) {
|
|
return {
|
|
canMount: false,
|
|
reason: `Requires ${vehicle.terrainRequired} terrain`
|
|
};
|
|
}
|
|
|
|
// Check if train requires tracks
|
|
if (vehicle.requiresTrack && !this.isOnTrack(playerX, playerY)) {
|
|
return { canMount: false, reason: 'Train requires tracks' };
|
|
}
|
|
|
|
// Check if cart/wagon requires horse
|
|
if (vehicle.requiresHorse && !this.hasHorse()) {
|
|
return { canMount: false, reason: 'Requires a horse to pull' };
|
|
}
|
|
|
|
return { canMount: true };
|
|
}
|
|
|
|
mount(vehicleId) {
|
|
const check = this.canMount(vehicleId, this.scene.player.x, this.scene.player.y);
|
|
if (!check.canMount) {
|
|
this.scene.uiSystem?.showError(check.reason);
|
|
return false;
|
|
}
|
|
|
|
const vehicle = this.vehicleData[vehicleId];
|
|
|
|
// Update player speed
|
|
this.scene.player.baseSpeed = this.scene.player.baseSpeed || 100;
|
|
this.scene.player.setMaxVelocity(vehicle.speed);
|
|
|
|
// Change player sprite (riding animation)
|
|
const originalTexture = this.scene.player.texture.key;
|
|
this.scene.player.setTexture(`player_on_${vehicle.type}`);
|
|
|
|
// Store original state
|
|
this.currentVehicle = {
|
|
id: vehicleId,
|
|
data: vehicle,
|
|
originalSpeed: this.scene.player.baseSpeed,
|
|
originalTexture: originalTexture,
|
|
mountTime: Date.now()
|
|
};
|
|
|
|
// Show UI
|
|
this.scene.uiSystem?.showVehicleUI({
|
|
name: vehicle.name,
|
|
speed: vehicle.speed,
|
|
capacity: vehicle.capacity,
|
|
currentCargo: 0
|
|
});
|
|
|
|
// Emit event
|
|
this.scene.events.emit('vehicleMounted', { vehicleId, vehicle });
|
|
|
|
console.log(`🚴 Mounted ${vehicle.name} (${vehicle.speed} speed)`);
|
|
return true;
|
|
}
|
|
|
|
dismount() {
|
|
if (!this.currentVehicle) {
|
|
return false;
|
|
}
|
|
|
|
const vehicle = this.currentVehicle.data;
|
|
|
|
// Restore player speed
|
|
this.scene.player.setMaxVelocity(this.currentVehicle.originalSpeed);
|
|
|
|
// Restore player sprite
|
|
this.scene.player.setTexture(this.currentVehicle.originalTexture);
|
|
|
|
// Hide UI
|
|
this.scene.uiSystem?.hideVehicleUI();
|
|
|
|
// Emit event
|
|
this.scene.events.emit('vehicleDismounted', {
|
|
vehicleId: this.currentVehicle.id,
|
|
rideDuration: Date.now() - this.currentVehicle.mountTime
|
|
});
|
|
|
|
console.log(`🚶 Dismounted ${vehicle.name}`);
|
|
this.currentVehicle = null;
|
|
return true;
|
|
}
|
|
|
|
// ===== VEHICLE PURCHASING =====
|
|
|
|
canPurchase(vehicleId) {
|
|
const vehicle = this.vehicleData[vehicleId];
|
|
if (!vehicle) {
|
|
return { canPurchase: false, reason: 'Vehicle not found' };
|
|
}
|
|
|
|
// Check if already owned
|
|
if (this.ownedVehicles.has(vehicleId)) {
|
|
return { canPurchase: false, reason: 'Already owned' };
|
|
}
|
|
|
|
// Check level
|
|
const playerLevel = this.scene.player?.stats?.level || 1;
|
|
if (playerLevel < vehicle.unlockLevel) {
|
|
return {
|
|
canPurchase: false,
|
|
reason: `Requires level ${vehicle.unlockLevel}`
|
|
};
|
|
}
|
|
|
|
// Check blueprint (if required)
|
|
if (vehicle.blueprint && this.scene.recipeSystem) {
|
|
if (!this.scene.recipeSystem.unlockedRecipes.has(vehicle.blueprint)) {
|
|
return { canPurchase: false, reason: 'Blueprint not unlocked' };
|
|
}
|
|
}
|
|
|
|
// Check gold
|
|
const playerGold = this.getPlayerGold();
|
|
if (playerGold < vehicle.cost) {
|
|
return {
|
|
canPurchase: false,
|
|
reason: `Need ${vehicle.cost} gold (have ${playerGold})`
|
|
};
|
|
}
|
|
|
|
return { canPurchase: true };
|
|
}
|
|
|
|
purchase(vehicleId) {
|
|
const check = this.canPurchase(vehicleId);
|
|
if (!check.canPurchase) {
|
|
this.scene.uiSystem?.showError(check.reason);
|
|
return false;
|
|
}
|
|
|
|
const vehicle = this.vehicleData[vehicleId];
|
|
|
|
// Deduct gold
|
|
this.removeGold(vehicle.cost);
|
|
|
|
// Add to owned vehicles
|
|
this.ownedVehicles.set(vehicleId, {
|
|
purchaseDate: Date.now(),
|
|
totalDistance: 0,
|
|
totalTime: 0
|
|
});
|
|
|
|
// Save
|
|
this.saveOwnedVehicles();
|
|
|
|
// Notification
|
|
this.scene.uiSystem?.showNotification({
|
|
title: '🚗 Vehicle Purchased!',
|
|
message: `${vehicle.name} is now yours!`,
|
|
icon: vehicle.sprite,
|
|
duration: 4000,
|
|
color: '#00FF00'
|
|
});
|
|
|
|
// Emit event
|
|
this.scene.events.emit('vehiclePurchased', { vehicleId, vehicle });
|
|
|
|
console.log(`💰 Purchased ${vehicle.name} for ${vehicle.cost} gold`);
|
|
return true;
|
|
}
|
|
|
|
// ===== TRAIN REPAIR SYSTEM =====
|
|
|
|
repairTrain(workAmount = 10) {
|
|
if (this.trainRepairProgress >= 100) {
|
|
this.scene.uiSystem?.showError('Train already repaired!');
|
|
return false;
|
|
}
|
|
|
|
// Add progress
|
|
this.trainRepairProgress += workAmount;
|
|
this.trainRepairProgress = Math.min(100, this.trainRepairProgress);
|
|
|
|
// Get current state
|
|
const state = this.getTrainRepairState();
|
|
|
|
// Show progress
|
|
this.scene.uiSystem?.showNotification({
|
|
title: 'Train Repair',
|
|
message: `${state} - ${this.trainRepairProgress.toFixed(0)}%`,
|
|
icon: 'train',
|
|
duration: 2000
|
|
});
|
|
|
|
// Check if just completed
|
|
if (this.trainRepairProgress === 100 && state === 'Rideable') {
|
|
this.unlockTrain();
|
|
}
|
|
|
|
// Emit event
|
|
this.scene.events.emit('trainRepairProgress', {
|
|
progress: this.trainRepairProgress,
|
|
state
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
getTrainRepairState() {
|
|
if (this.trainRepairProgress < 33) {
|
|
return 'Broken';
|
|
} else if (this.trainRepairProgress < 100) {
|
|
return 'Repairing';
|
|
} else {
|
|
return 'Rideable';
|
|
}
|
|
}
|
|
|
|
unlockTrain() {
|
|
// Add train to owned vehicles
|
|
this.ownedVehicles.set('train', {
|
|
purchaseDate: Date.now(),
|
|
totalDistance: 0,
|
|
totalTime: 0
|
|
});
|
|
|
|
this.saveOwnedVehicles();
|
|
|
|
// Big notification
|
|
this.scene.uiSystem?.showNotification({
|
|
title: '🎉 TRAIN REPAIRED!',
|
|
message: 'The steam train is ready to ride!',
|
|
icon: 'train',
|
|
duration: 6000,
|
|
color: '#FFD700'
|
|
});
|
|
|
|
this.scene.events.emit('trainUnlocked');
|
|
console.log('🚂 Train fully repaired and unlocked!');
|
|
}
|
|
|
|
// ===== TERRAIN BONUS SYSTEM =====
|
|
|
|
getSpeedModifier(x, y) {
|
|
if (!this.currentVehicle) return 1.0;
|
|
|
|
const vehicle = this.currentVehicle.data;
|
|
const tile = this.getTileAt(x, y);
|
|
const terrain = tile.properties?.terrain;
|
|
|
|
if (vehicle.terrainBonus && terrain && vehicle.terrainBonus[terrain]) {
|
|
return vehicle.terrainBonus[terrain];
|
|
}
|
|
|
|
return 1.0;
|
|
}
|
|
|
|
getCurrentSpeed() {
|
|
if (!this.currentVehicle) {
|
|
return this.scene.player?.baseSpeed || 100;
|
|
}
|
|
|
|
const baseSpeed = this.currentVehicle.data.speed;
|
|
const modifier = this.getSpeedModifier(this.scene.player.x, this.scene.player.y);
|
|
|
|
return baseSpeed * modifier;
|
|
}
|
|
|
|
// ===== HELPER FUNCTIONS =====
|
|
|
|
hasHorse() {
|
|
return this.ownedVehicles.has('horse_basic') ||
|
|
this.ownedVehicles.has('horse_fast') ||
|
|
this.ownedVehicles.has('horse_draft');
|
|
}
|
|
|
|
isOnTrack(x, y) {
|
|
// Check if position has train tracks
|
|
const tile = this.getTileAt(x, y);
|
|
return tile.properties?.hasTrack || false;
|
|
}
|
|
|
|
getTileAt(x, y) {
|
|
// Get tile from terrain system
|
|
if (this.scene.terrainSystem) {
|
|
return this.scene.terrainSystem.getTileAtWorldXY(x, y);
|
|
}
|
|
|
|
// Fallback
|
|
return { properties: {} };
|
|
}
|
|
|
|
getPlayerGold() {
|
|
if (this.scene.inventorySystem) {
|
|
return this.scene.inventorySystem.getQuantity('gold');
|
|
}
|
|
return this.scene.player?.inventory?.gold || 0;
|
|
}
|
|
|
|
removeGold(amount) {
|
|
if (this.scene.inventorySystem) {
|
|
this.scene.inventorySystem.removeItem('gold', amount);
|
|
} else {
|
|
this.scene.player.inventory.gold -= amount;
|
|
}
|
|
}
|
|
|
|
// ===== GETTERS =====
|
|
|
|
getVehicle(vehicleId) {
|
|
return this.vehicleData[vehicleId];
|
|
}
|
|
|
|
getAllVehicles() {
|
|
return Object.entries(this.vehicleData).map(([id, data]) => ({
|
|
id,
|
|
...data,
|
|
owned: this.ownedVehicles.has(id)
|
|
}));
|
|
}
|
|
|
|
getOwnedVehicles() {
|
|
return Array.from(this.ownedVehicles.keys());
|
|
}
|
|
|
|
getVehiclesByType(type) {
|
|
return this.getAllVehicles().filter(v => v.type === type);
|
|
}
|
|
|
|
getPurchasableVehicles() {
|
|
return this.getAllVehicles().filter(v =>
|
|
this.canPurchase(v.id).canPurchase
|
|
);
|
|
}
|
|
|
|
getCurrentVehicle() {
|
|
return this.currentVehicle;
|
|
}
|
|
|
|
isRiding() {
|
|
return this.currentVehicle !== null;
|
|
}
|
|
|
|
// ===== DATA PERSISTENCE =====
|
|
|
|
saveOwnedVehicles() {
|
|
const data = {
|
|
owned: Array.from(this.ownedVehicles.entries()),
|
|
trainProgress: this.trainRepairProgress
|
|
};
|
|
localStorage.setItem('ownedVehicles', JSON.stringify(data));
|
|
}
|
|
|
|
loadOwnedVehicles() {
|
|
const saved = localStorage.getItem('ownedVehicles');
|
|
if (saved) {
|
|
const data = JSON.parse(saved);
|
|
this.ownedVehicles = new Map(data.owned);
|
|
this.trainRepairProgress = data.trainProgress || 0;
|
|
console.log('🚗 Loaded', this.ownedVehicles.size, 'owned vehicles');
|
|
}
|
|
}
|
|
|
|
// ===== UPDATE =====
|
|
|
|
update(time, delta) {
|
|
if (this.currentVehicle) {
|
|
// Track distance and time
|
|
const deltaSeconds = delta / 1000;
|
|
const speed = this.getCurrentSpeed();
|
|
const distance = (speed * deltaSeconds) / 60; // Approximate
|
|
|
|
const stats = this.ownedVehicles.get(this.currentVehicle.id);
|
|
if (stats) {
|
|
stats.totalDistance += distance;
|
|
stats.totalTime += deltaSeconds;
|
|
}
|
|
|
|
// Update speed modifier based on terrain
|
|
const modifier = this.getSpeedModifier(this.scene.player.x, this.scene.player.y);
|
|
if (modifier !== 1.0) {
|
|
this.scene.player.setMaxVelocity(this.currentVehicle.data.speed * modifier);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ===== CLEANUP =====
|
|
|
|
destroy() {
|
|
this.saveOwnedVehicles();
|
|
this.ownedVehicles.clear();
|
|
this.trainTracks = [];
|
|
this.currentVehicle = null;
|
|
}
|
|
}
|