Browse Source

Add death system fixes, movement spawning, home regen

- Fix death system: add clearMonsterEntourage(), set HP to 0 on death
- Add movement-based monster spawning (configurable distance)
- Add admin-editable spawn settings (interval, chance, distance)
- Add home base HP/MP regeneration when player is nearby
- Update character sheet to show damage range, accuracy, MP cost
- Change WASD test step from 11m to 1m
- Fix monster spawning after respawn

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
master
HikeMap User 1 month ago
parent
commit
cacb7d6d22
  1. 37
      admin.html
  2. 123
      database.js
  3. 855
      index.html
  4. 154
      server.js
  5. 39
      to_do.md

37
admin.html

@ -762,12 +762,26 @@
<h3>Monster Spawning</h3> <h3>Monster Spawning</h3>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>Spawn Interval (ms)</label>
<input type="number" id="setting-monsterSpawnInterval" placeholder="30000">
<label>Spawn Interval (seconds)</label>
<input type="number" id="setting-monsterSpawnInterval" placeholder="20" min="5" step="1">
<small style="color: #888; font-size: 11px;">How often spawn attempts occur</small>
</div>
<div class="form-group">
<label>Spawn Chance (%)</label>
<input type="number" id="setting-monsterSpawnChance" placeholder="50" min="1" max="100">
<small style="color: #888; font-size: 11px;">Percent chance per interval</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Movement Distance (meters)</label>
<input type="number" id="setting-monsterSpawnDistance" placeholder="10" min="1">
<small style="color: #888; font-size: 11px;">Distance player must move for new spawns</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Max Monsters Per Player</label> <label>Max Monsters Per Player</label>
<input type="number" id="setting-maxMonstersPerPlayer" placeholder="10">
<input type="number" id="setting-maxMonstersPerPlayer" placeholder="10" min="1">
<small style="color: #888; font-size: 11px;">Maximum monsters following player</small>
</div> </div>
</div> </div>
</div> </div>
@ -1845,8 +1859,11 @@
const data = await api('/api/admin/settings'); const data = await api('/api/admin/settings');
settings = data.settings || {}; settings = data.settings || {};
// Populate form
document.getElementById('setting-monsterSpawnInterval').value = settings.monsterSpawnInterval || 30000;
// Populate form (convert interval from ms to seconds for display)
const intervalMs = settings.monsterSpawnInterval || 20000;
document.getElementById('setting-monsterSpawnInterval').value = Math.round(intervalMs / 1000);
document.getElementById('setting-monsterSpawnChance').value = settings.monsterSpawnChance || 50;
document.getElementById('setting-monsterSpawnDistance').value = settings.monsterSpawnDistance || 10;
document.getElementById('setting-maxMonstersPerPlayer').value = settings.maxMonstersPerPlayer || 10; document.getElementById('setting-maxMonstersPerPlayer').value = settings.maxMonstersPerPlayer || 10;
document.getElementById('setting-xpMultiplier').value = settings.xpMultiplier || 1.0; document.getElementById('setting-xpMultiplier').value = settings.xpMultiplier || 1.0;
document.getElementById('setting-combatEnabled').checked = settings.combatEnabled !== 'false' && settings.combatEnabled !== false; document.getElementById('setting-combatEnabled').checked = settings.combatEnabled !== 'false' && settings.combatEnabled !== false;
@ -1856,10 +1873,14 @@
} }
document.getElementById('saveSettingsBtn').addEventListener('click', async () => { document.getElementById('saveSettingsBtn').addEventListener('click', async () => {
// Convert interval from seconds to ms for storage
const intervalSeconds = parseInt(document.getElementById('setting-monsterSpawnInterval').value) || 20;
const newSettings = { const newSettings = {
monsterSpawnInterval: document.getElementById('setting-monsterSpawnInterval').value,
maxMonstersPerPlayer: document.getElementById('setting-maxMonstersPerPlayer').value,
xpMultiplier: document.getElementById('setting-xpMultiplier').value,
monsterSpawnInterval: intervalSeconds * 1000,
monsterSpawnChance: parseInt(document.getElementById('setting-monsterSpawnChance').value) || 50,
monsterSpawnDistance: parseInt(document.getElementById('setting-monsterSpawnDistance').value) || 10,
maxMonstersPerPlayer: parseInt(document.getElementById('setting-maxMonstersPerPlayer').value) || 10,
xpMultiplier: parseFloat(document.getElementById('setting-xpMultiplier').value) || 1.0,
combatEnabled: document.getElementById('setting-combatEnabled').checked combatEnabled: document.getElementById('setting-combatEnabled').checked
}; };

123
database.js

@ -209,6 +209,23 @@ class HikeMapDB {
this.db.exec(`ALTER TABLE monster_skills ADD COLUMN custom_name TEXT`); this.db.exec(`ALTER TABLE monster_skills ADD COLUMN custom_name TEXT`);
} catch (e) { /* Column already exists */ } } catch (e) { /* Column already exists */ }
// Migration: Add home base and death system columns to rpg_stats
try {
this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN home_base_lat REAL`);
} catch (e) { /* Column already exists */ }
try {
this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN home_base_lng REAL`);
} catch (e) { /* Column already exists */ }
try {
this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN last_home_set TEXT`);
} catch (e) { /* Column already exists */ }
try {
this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN is_dead INTEGER DEFAULT 0`);
} catch (e) { /* Column already exists */ }
try {
this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN home_base_icon TEXT DEFAULT '00'`);
} catch (e) { /* Column already exists */ }
// Game settings table - key/value store for game configuration // Game settings table - key/value store for game configuration
this.db.exec(` this.db.exec(`
CREATE TABLE IF NOT EXISTS game_settings ( CREATE TABLE IF NOT EXISTS game_settings (
@ -468,7 +485,8 @@ class HikeMapDB {
// RPG Stats methods // RPG Stats methods
getRpgStats(userId) { getRpgStats(userId) {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
SELECT character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, accuracy, dodge, unlocked_skills
SELECT character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, accuracy, dodge, unlocked_skills,
home_base_lat, home_base_lng, last_home_set, is_dead, home_base_icon
FROM rpg_stats WHERE user_id = ? FROM rpg_stats WHERE user_id = ?
`); `);
return stmt.get(userId); return stmt.get(userId);
@ -525,8 +543,8 @@ class HikeMapDB {
saveRpgStats(userId, stats) { saveRpgStats(userId, stats) {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO rpg_stats (user_id, character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, accuracy, dodge, unlocked_skills, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
INSERT INTO rpg_stats (user_id, character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, accuracy, dodge, unlocked_skills, home_base_lat, home_base_lng, last_home_set, is_dead, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT(user_id) DO UPDATE SET ON CONFLICT(user_id) DO UPDATE SET
character_name = COALESCE(excluded.character_name, rpg_stats.character_name), character_name = COALESCE(excluded.character_name, rpg_stats.character_name),
race = COALESCE(excluded.race, rpg_stats.race), race = COALESCE(excluded.race, rpg_stats.race),
@ -542,6 +560,10 @@ class HikeMapDB {
accuracy = excluded.accuracy, accuracy = excluded.accuracy,
dodge = excluded.dodge, dodge = excluded.dodge,
unlocked_skills = COALESCE(excluded.unlocked_skills, rpg_stats.unlocked_skills), unlocked_skills = COALESCE(excluded.unlocked_skills, rpg_stats.unlocked_skills),
home_base_lat = COALESCE(excluded.home_base_lat, rpg_stats.home_base_lat),
home_base_lng = COALESCE(excluded.home_base_lng, rpg_stats.home_base_lng),
last_home_set = COALESCE(excluded.last_home_set, rpg_stats.last_home_set),
is_dead = COALESCE(excluded.is_dead, rpg_stats.is_dead),
updated_at = datetime('now') updated_at = datetime('now')
`); `);
// Convert unlockedSkills array to JSON string for storage // Convert unlockedSkills array to JSON string for storage
@ -561,10 +583,94 @@ class HikeMapDB {
stats.def || 8, stats.def || 8,
stats.accuracy || 90, stats.accuracy || 90,
stats.dodge || 10, stats.dodge || 10,
unlockedSkillsJson
unlockedSkillsJson,
stats.homeBaseLat || null,
stats.homeBaseLng || null,
stats.lastHomeSet || null,
stats.isDead !== undefined ? (stats.isDead ? 1 : 0) : null
); );
} }
// Set home base location
setHomeBase(userId, lat, lng) {
const stmt = this.db.prepare(`
UPDATE rpg_stats SET
home_base_lat = ?,
home_base_lng = ?,
last_home_set = datetime('now'),
updated_at = datetime('now')
WHERE user_id = ?
`);
return stmt.run(lat, lng, userId);
}
// Update home base icon
setHomeBaseIcon(userId, iconId) {
const stmt = this.db.prepare(`
UPDATE rpg_stats SET
home_base_icon = ?,
updated_at = datetime('now')
WHERE user_id = ?
`);
return stmt.run(iconId, userId);
}
// Check if user can set home base (once per day)
canSetHomeBase(userId) {
const stmt = this.db.prepare(`
SELECT last_home_set FROM rpg_stats WHERE user_id = ?
`);
const result = stmt.get(userId);
if (!result || !result.last_home_set) return true;
const lastSet = new Date(result.last_home_set);
const now = new Date();
const hoursSince = (now - lastSet) / (1000 * 60 * 60);
return hoursSince >= 24;
}
// Handle player death
handlePlayerDeath(userId, xpPenaltyPercent = 10) {
// Get current stats to calculate XP penalty
const stats = this.getRpgStats(userId);
if (!stats) return null;
// Calculate XP loss - can't drop below current level threshold
const currentLevel = stats.level;
const levelThresholds = [0, 100, 250, 500, 800, 1200]; // XP needed for each level
const minXp = levelThresholds[currentLevel - 1] || 0;
const xpLoss = Math.floor(stats.xp * (xpPenaltyPercent / 100));
const newXp = Math.max(minXp, stats.xp - xpLoss);
const stmt = this.db.prepare(`
UPDATE rpg_stats SET
is_dead = 1,
hp = 0,
xp = ?,
updated_at = datetime('now')
WHERE user_id = ?
`);
stmt.run(newXp, userId);
return { xpLost: stats.xp - newXp, newXp };
}
// Respawn player at home base
respawnPlayer(userId) {
const stats = this.getRpgStats(userId);
if (!stats) return null;
const stmt = this.db.prepare(`
UPDATE rpg_stats SET
is_dead = 0,
hp = max_hp,
mp = max_mp,
updated_at = datetime('now')
WHERE user_id = ?
`);
return stmt.run(userId);
}
// Monster entourage methods // Monster entourage methods
getMonsterEntourage(userId) { getMonsterEntourage(userId) {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
@ -611,6 +717,11 @@ class HikeMapDB {
return stmt.run(userId, monsterId); return stmt.run(userId, monsterId);
} }
clearMonsterEntourage(userId) {
const stmt = this.db.prepare(`DELETE FROM monster_entourage WHERE user_id = ?`);
return stmt.run(userId);
}
// Monster type methods // Monster type methods
getAllMonsterTypes(enabledOnly = true) { getAllMonsterTypes(enabledOnly = true) {
const stmt = enabledOnly const stmt = enabledOnly
@ -1193,7 +1304,9 @@ class HikeMapDB {
seedDefaultSettings() { seedDefaultSettings() {
const defaults = { const defaults = {
monsterSpawnInterval: 30000,
monsterSpawnInterval: 20000, // Timer interval in ms (20 seconds)
monsterSpawnChance: 50, // Percent chance per interval (50%)
monsterSpawnDistance: 10, // Meters player must move for new spawns (10m)
maxMonstersPerPlayer: 10, maxMonstersPerPlayer: 10,
xpMultiplier: 1.0, xpMultiplier: 1.0,
combatEnabled: true combatEnabled: true

855
index.html

@ -2401,6 +2401,229 @@
left: 0; left: 0;
} }
/* Home Base Button */
.home-base-btn {
position: fixed;
bottom: 100px;
right: 15px;
width: 50px;
height: 50px;
border-radius: 50%;
background: linear-gradient(135deg, #4a90d9 0%, #357abd 100%);
border: 3px solid #fff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
cursor: pointer;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
transition: all 0.2s;
}
.home-base-btn:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
}
.home-base-btn.selecting {
background: linear-gradient(135deg, #f39c12 0%, #d68910 100%);
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(243, 156, 18, 0.7); }
50% { box-shadow: 0 0 0 15px rgba(243, 156, 18, 0); }
}
/* Home Base Marker */
.home-base-marker {
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.home-base-marker img {
width: 50px;
height: 50px;
object-fit: contain;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
}
/* Death Overlay */
.death-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 2000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
text-align: center;
padding: 20px;
}
.death-overlay h1 {
font-size: 48px;
color: #e94560;
margin-bottom: 20px;
text-shadow: 0 0 20px rgba(233, 69, 96, 0.8);
}
.death-overlay p {
font-size: 18px;
margin-bottom: 10px;
max-width: 300px;
}
.death-overlay .xp-lost {
color: #e94560;
font-size: 24px;
margin: 20px 0;
}
.death-overlay .home-distance {
margin-top: 20px;
font-size: 16px;
color: #4ecdc4;
}
/* Dead state HUD styling */
.rpg-hud.dead {
filter: grayscale(100%);
opacity: 0.6;
}
.rpg-hud.dead::after {
content: '💀';
position: absolute;
top: -10px;
right: -10px;
font-size: 24px;
}
/* Home base selection mode hint */
.selection-hint {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: rgba(243, 156, 18, 0.95);
color: white;
padding: 12px 24px;
border-radius: 25px;
font-weight: bold;
z-index: 1001;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
/* Homebase Customization Modal */
.homebase-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 3000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.homebase-modal-content {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 16px;
padding: 24px;
max-width: 400px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}
.homebase-modal h2 {
color: #4ecdc4;
text-align: center;
margin-bottom: 20px;
font-size: 24px;
}
.homebase-modal h3 {
color: #fff;
margin: 16px 0 12px;
font-size: 16px;
}
.homebase-icons-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.homebase-icon-option {
aspect-ratio: 1;
border-radius: 12px;
background: rgba(255, 255, 255, 0.1);
border: 3px solid transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
padding: 8px;
}
.homebase-icon-option:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
}
.homebase-icon-option.selected {
border-color: #4ecdc4;
background: rgba(78, 205, 196, 0.2);
}
.homebase-icon-option img {
width: 100%;
height: 100%;
object-fit: contain;
}
.homebase-modal-actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
.homebase-modal-actions button {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.homebase-btn-relocate {
background: linear-gradient(135deg, #f39c12 0%, #d68910 100%);
color: white;
}
.homebase-btn-relocate:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(243, 156, 18, 0.4);
}
.homebase-btn-relocate:disabled {
background: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.homebase-btn-close {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.homebase-btn-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.homebase-relocate-cooldown {
text-align: center;
color: #888;
font-size: 12px;
margin-top: 8px;
}
</style> </style>
</head> </head>
<body> <body>
@ -2432,6 +2655,38 @@
</div> </div>
</div> </div>
<!-- Home Base Button -->
<button id="homeBaseBtn" class="home-base-btn" style="display: none;" onclick="toggleHomeBaseSelection()" title="Set Home Base">🏠</button>
<!-- Home Base Selection Hint -->
<div id="selectionHint" class="selection-hint" style="display: none;">Tap on the map to set your home base</div>
<!-- Death Overlay -->
<div id="deathOverlay" class="death-overlay" style="display: none;">
<h1>💀 YOU DIED 💀</h1>
<p>Return to your home base to respawn.</p>
<div class="xp-lost" id="xpLostText">-0 XP</div>
<div class="home-distance" id="homeDistanceText">Distance to home: ???</div>
</div>
<!-- Homebase Customization Modal -->
<div id="homebaseModal" class="homebase-modal" style="display: none;">
<div class="homebase-modal-content">
<h2>🏠 Homebase</h2>
<h3>Select Icon</h3>
<div id="homebaseIconsGrid" class="homebase-icons-grid">
<!-- Icons loaded dynamically -->
</div>
<div class="homebase-modal-actions">
<button id="relocateBtn" class="homebase-btn-relocate" onclick="startRelocateHomebase()">Relocate</button>
<button class="homebase-btn-close" onclick="closeHomebaseModal()">Close</button>
</div>
<div id="relocateCooldown" class="homebase-relocate-cooldown"></div>
</div>
</div>
<!-- Combat Overlay --> <!-- Combat Overlay -->
<div id="combatOverlay" class="combat-overlay" style="display: none;"> <div id="combatOverlay" class="combat-overlay" style="display: none;">
<div class="combat-container"> <div class="combat-container">
@ -3220,7 +3475,7 @@
// GPS test mode (admin only) // GPS test mode (admin only)
let gpsTestMode = false; let gpsTestMode = false;
let testPosition = { lat: 37.7749, lng: -122.4194 }; // Default to SF let testPosition = { lat: 37.7749, lng: -122.4194 }; // Default to SF
const GPS_TEST_STEP = 0.0001; // ~11 meters per step
const GPS_TEST_STEP = 0.000009; // ~1 meter per step
// Navigation state // Navigation state
let navMode = false; let navMode = false;
@ -3461,6 +3716,9 @@
xpReward: t.xpReward, xpReward: t.xpReward,
accuracy: t.accuracy || 85, accuracy: t.accuracy || 85,
dodge: t.dodge || 5, dodge: t.dodge || 5,
minLevel: t.minLevel || 1,
maxLevel: t.maxLevel || 99,
spawnWeight: t.spawnWeight || 100,
levelScale: t.levelScale levelScale: t.levelScale
}; };
MONSTER_DIALOGUES[t.id] = t.dialogues; MONSTER_DIALOGUES[t.id] = t.dialogues;
@ -3473,6 +3731,22 @@
} }
} }
// Load spawn settings from server
async function loadSpawnSettings() {
try {
const response = await fetch('/api/spawn-settings');
if (response.ok) {
const settings = await response.json();
spawnSettings.spawnInterval = settings.spawnInterval || 20000;
spawnSettings.spawnChance = settings.spawnChance || 50;
spawnSettings.spawnDistance = settings.spawnDistance || 10;
console.log('Loaded spawn settings:', spawnSettings);
}
} catch (err) {
console.error('Failed to load spawn settings:', err);
}
}
// Load skills from database // Load skills from database
async function loadSkillsFromDatabase() { async function loadSkillsFromDatabase() {
try { try {
@ -3616,6 +3890,23 @@
let combatState = null; // Active combat state or null let combatState = null; // Active combat state or null
let monsterSpawnTimer = null; // Interval for spawning monsters let monsterSpawnTimer = null; // Interval for spawning monsters
let monsterUpdateTimer = null; // Interval for updating monster positions/dialogue let monsterUpdateTimer = null; // Interval for updating monster positions/dialogue
let lastSpawnLocation = null; // Track player location at last spawn (for movement-based spawning)
// Spawn settings (loaded from server, with defaults)
let spawnSettings = {
spawnInterval: 20000, // Timer interval in ms
spawnChance: 50, // Percent chance per interval
spawnDistance: 10 // Meters player must move
};
// Home Base state variables
let homeBaseMarker = null; // Leaflet marker for home base
let homeBaseSelectionMode = false; // Whether we're in home base selection mode
let xpLostOnDeath = 0; // Track XP lost for display
let lastHomeRegenTime = 0; // Track last HP/MP regen at home base
const HOME_REGEN_INTERVAL = 3000; // Regen every 3 seconds when at home
const HOME_REGEN_PERCENT = 5; // Regen 5% of max HP/MP per tick
const HOME_BASE_RADIUS = 20; // Meters - radius for home base effects
// Find nearest monster to a location (for double-tap to battle on mobile) // Find nearest monster to a location (for double-tap to battle on mobile)
function findNearestMonster(latlng, maxDistanceMeters = 50) { function findNearestMonster(latlng, maxDistanceMeters = 50) {
@ -3810,6 +4101,12 @@
// Store user location for geocache proximity checks // Store user location for geocache proximity checks
userLocation = { lat, lng, accuracy }; userLocation = { lat, lng, accuracy };
// Check if dead player has reached home base for respawn
checkHomeBaseRespawn();
// Check for HP/MP regeneration at home base
checkHomeBaseRegen();
// Update geocache visibility based on new location // Update geocache visibility based on new location
if (navMode) { if (navMode) {
updateGeocacheVisibility(); updateGeocacheVisibility();
@ -7378,6 +7675,12 @@
// Map click handler // Map click handler
map.on('click', (e) => { map.on('click', (e) => {
// Handle home base selection mode
if (homeBaseSelectionMode) {
setHomeBase(e.latlng.lat, e.latlng.lng);
return;
}
// In navigation mode, clicks are handled by press-and-hold // In navigation mode, clicks are handled by press-and-hold
if (navMode) { if (navMode) {
return; return;
@ -9394,15 +9697,53 @@
// Update skills (show only unlocked skills) // Update skills (show only unlocked skills)
const unlockedSkills = playerStats.unlockedSkills || ['basic_attack']; const unlockedSkills = playerStats.unlockedSkills || ['basic_attack'];
document.getElementById('charSheetSkills').innerHTML = unlockedSkills.map(skillId => { document.getElementById('charSheetSkills').innerHTML = unlockedSkills.map(skillId => {
const skill = SKILLS[skillId];
const dbSkill = SKILLS_DB[skillId];
const hardcodedSkill = SKILLS[skillId];
const skill = dbSkill || hardcodedSkill;
if (!skill) return ''; if (!skill) return '';
// Calculate skill stats for display
let statsText = '';
const accuracy = dbSkill?.accuracy || 95;
const mpCost = skill.mpCost || 0;
if (skill.type === 'damage') {
// Calculate damage based on player ATK
let damage;
const hits = skill.hitCount || skill.hits || 1;
if (hardcodedSkill && hardcodedSkill.calculate) {
damage = hardcodedSkill.calculate(playerStats.atk);
} else if (dbSkill) {
damage = Math.floor(playerStats.atk * (dbSkill.basePower / 100));
} else {
damage = playerStats.atk;
}
const minDmg = Math.max(1, Math.floor(damage * 0.9));
const maxDmg = Math.floor(damage * 1.1);
if (hits > 1) {
statsText = `${minDmg}-${maxDmg} x${hits} | ${accuracy}% | ${mpCost} MP`;
} else {
statsText = `${minDmg}-${maxDmg} dmg | ${accuracy}% | ${mpCost} MP`;
}
} else if (skill.type === 'heal') {
const healAmount = hardcodedSkill?.calculate ? hardcodedSkill.calculate(playerStats.maxHp) : 0;
statsText = `+${healAmount} HP | ${mpCost} MP`;
} else if (skill.type === 'restore') {
const restoreAmount = hardcodedSkill?.calculate ? hardcodedSkill.calculate(playerStats.maxMp) : 0;
statsText = `+${restoreAmount} MP | ${mpCost} MP`;
} else if (skill.type === 'buff') {
statsText = `Buff | ${mpCost} MP`;
} else {
statsText = `${mpCost} MP`;
}
return ` return `
<div class="char-sheet-skill"> <div class="char-sheet-skill">
<span class="skill-icon">${skill.icon}</span> <span class="skill-icon">${skill.icon}</span>
<div class="skill-info"> <div class="skill-info">
<div class="skill-name">${skill.name}</div> <div class="skill-name">${skill.name}</div>
<div class="skill-desc">${skill.description}</div> <div class="skill-desc">${skill.description}</div>
<div class="skill-cost">${skill.mpCost} MP</div>
<div class="skill-cost">${statsText}</div>
</div> </div>
</div> </div>
`; `;
@ -9673,7 +10014,14 @@
// Show RPG HUD and start game // Show RPG HUD and start game
document.getElementById('rpgHud').style.display = 'flex'; document.getElementById('rpgHud').style.display = 'flex';
updateRpgHud(); updateRpgHud();
updateHomeBaseMarker();
// If player is dead, show death overlay
if (playerStats.isDead) {
document.getElementById('deathOverlay').style.display = 'flex';
} else {
startMonsterSpawning(); startMonsterSpawning();
}
console.log('RPG system initialized for', username); console.log('RPG system initialized for', username);
return; return;
@ -9694,7 +10042,14 @@
document.getElementById('rpgHud').style.display = 'flex'; document.getElementById('rpgHud').style.display = 'flex';
updateRpgHud(); updateRpgHud();
updateHomeBaseMarker();
// If player is dead, show death overlay
if (playerStats.isDead) {
document.getElementById('deathOverlay').style.display = 'flex';
} else {
startMonsterSpawning(); startMonsterSpawning();
}
return; return;
} }
} catch (e) { } catch (e) {
@ -9770,6 +10125,442 @@
const xpPercent = Math.min(100, (playerStats.xp / xpNeeded) * 100); const xpPercent = Math.min(100, (playerStats.xp / xpNeeded) * 100);
document.getElementById('hudXpBar').style.width = xpPercent + '%'; document.getElementById('hudXpBar').style.width = xpPercent + '%';
document.getElementById('hudXpText').textContent = `${playerStats.xp}/${xpNeeded}`; document.getElementById('hudXpText').textContent = `${playerStats.xp}/${xpNeeded}`;
// Update dead state styling
const hud = document.getElementById('rpgHud');
if (playerStats.isDead) {
hud.classList.add('dead');
} else {
hud.classList.remove('dead');
}
// Show/hide home base button
document.getElementById('homeBaseBtn').style.display = playerStats ? 'flex' : 'none';
}
// ==========================================
// HOME BASE SYSTEM
// ==========================================
// Create or update home base marker on map
function updateHomeBaseMarker() {
if (!playerStats || !playerStats.homeBaseLat || !playerStats.homeBaseLng) {
if (homeBaseMarker) {
map.removeLayer(homeBaseMarker);
homeBaseMarker = null;
}
updateHomeBaseButton();
return;
}
// Use selected icon or default to '00' (use 100px, CSS scales to 50)
const iconId = playerStats.homeBaseIcon || '00';
const iconSrc = `/mapgameimgs/homebase${iconId}-100.png`;
const iconHtml = `
<div class="home-base-marker">
<img src="${iconSrc}" alt="Home Base" style="width:50px;height:50px;"
onerror="this.src='/mapgameimgs/default50.png'">
</div>
`;
const divIcon = L.divIcon({
html: iconHtml,
iconSize: [50, 50],
iconAnchor: [25, 25],
className: 'home-base-icon'
});
if (homeBaseMarker) {
// Update the icon (need to recreate the marker to change icon)
homeBaseMarker.setLatLng([playerStats.homeBaseLat, playerStats.homeBaseLng]);
homeBaseMarker.setIcon(divIcon);
} else {
homeBaseMarker = L.marker([playerStats.homeBaseLat, playerStats.homeBaseLng], {
icon: divIcon,
interactive: false
}).addTo(map);
}
updateHomeBaseButton();
}
// Update home base button text based on whether home is set
function updateHomeBaseButton() {
const btn = document.getElementById('homeBaseBtn');
if (playerStats && playerStats.homeBaseLat && playerStats.homeBaseLng) {
btn.innerHTML = '🏠';
btn.title = 'Homebase Settings';
} else {
btn.innerHTML = '🏠';
btn.title = 'Set Home Base';
}
}
// Toggle home base selection mode OR open customization modal
async function toggleHomeBaseSelection() {
// If in selection mode, cancel it
if (homeBaseSelectionMode) {
homeBaseSelectionMode = false;
document.getElementById('homeBaseBtn').classList.remove('selecting');
document.getElementById('selectionHint').style.display = 'none';
return;
}
// If home base is already set, open the customization modal
if (playerStats && playerStats.homeBaseLat && playerStats.homeBaseLng) {
openHomebaseModal();
return;
}
// Otherwise, start setting home base
await startSettingHomeBase();
}
// Start the home base selection process
async function startSettingHomeBase() {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/can-set-home', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (!data.canSet) {
alert('You can only set your home base once per day. Try again tomorrow!');
return;
}
// Enter selection mode
homeBaseSelectionMode = true;
document.getElementById('homeBaseBtn').classList.add('selecting');
document.getElementById('selectionHint').style.display = 'block';
} catch (err) {
console.error('Failed to check home base availability:', err);
}
}
// Open the homebase customization modal
async function openHomebaseModal() {
const modal = document.getElementById('homebaseModal');
modal.style.display = 'flex';
// Load available icons
await loadHomebaseIcons();
// Check if relocation is available
await updateRelocateButton();
}
// Close the homebase modal
function closeHomebaseModal() {
document.getElementById('homebaseModal').style.display = 'none';
}
// Load available homebase icons from server
async function loadHomebaseIcons() {
const grid = document.getElementById('homebaseIconsGrid');
grid.innerHTML = '<div style="color: #888; text-align: center; grid-column: 1/-1;">Loading icons...</div>';
try {
const response = await fetch('/api/homebase-icons');
const icons = await response.json();
if (icons.length === 0) {
grid.innerHTML = '<div style="color: #888; text-align: center; grid-column: 1/-1;">No homebase icons found</div>';
return;
}
grid.innerHTML = icons.map(icon => `
<div class="homebase-icon-option ${playerStats.homeBaseIcon === icon.id ? 'selected' : ''}"
onclick="selectHomebaseIcon('${icon.id}')"
data-icon-id="${icon.id}">
<img src="${icon.preview}" alt="Homebase ${icon.id}"
onerror="this.src='/mapgameimgs/default50.png'">
</div>
`).join('');
} catch (err) {
console.error('Failed to load homebase icons:', err);
grid.innerHTML = '<div style="color: #e94560; text-align: center; grid-column: 1/-1;">Failed to load icons</div>';
}
}
// Select a homebase icon
async function selectHomebaseIcon(iconId) {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/home-base/icon', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ iconId })
});
if (response.ok) {
playerStats.homeBaseIcon = iconId;
// Update selected state in UI
document.querySelectorAll('.homebase-icon-option').forEach(el => {
el.classList.toggle('selected', el.dataset.iconId === iconId);
});
// Update the marker on the map
updateHomeBaseMarker();
console.log('Home base icon updated to:', iconId);
}
} catch (err) {
console.error('Failed to update homebase icon:', err);
}
}
// Update the relocate button based on cooldown
async function updateRelocateButton() {
const btn = document.getElementById('relocateBtn');
const cooldownText = document.getElementById('relocateCooldown');
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/can-set-home', {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (data.canSet) {
btn.disabled = false;
cooldownText.textContent = '';
} else {
btn.disabled = true;
// Calculate time remaining
if (playerStats.lastHomeSet) {
const lastSet = new Date(playerStats.lastHomeSet);
const nextAvailable = new Date(lastSet.getTime() + 24 * 60 * 60 * 1000);
const now = new Date();
const hoursLeft = Math.ceil((nextAvailable - now) / (1000 * 60 * 60));
cooldownText.textContent = `Available in ~${hoursLeft} hour${hoursLeft !== 1 ? 's' : ''}`;
} else {
cooldownText.textContent = 'Try again tomorrow';
}
}
} catch (err) {
console.error('Failed to check relocate availability:', err);
}
}
// Start relocating homebase
async function startRelocateHomebase() {
closeHomebaseModal();
await startSettingHomeBase();
}
// Set home base at the given location
async function setHomeBase(lat, lng) {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/home-base', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ lat, lng })
});
if (response.ok) {
const data = await response.json();
playerStats.homeBaseLat = data.homeBaseLat;
playerStats.homeBaseLng = data.homeBaseLng;
updateHomeBaseMarker();
console.log('Home base set at:', lat, lng);
} else {
const error = await response.json();
alert(error.error || 'Failed to set home base');
}
} catch (err) {
console.error('Failed to set home base:', err);
}
// Exit selection mode
homeBaseSelectionMode = false;
document.getElementById('homeBaseBtn').classList.remove('selecting');
document.getElementById('selectionHint').style.display = 'none';
}
// Calculate distance to home base in meters
function getDistanceToHome() {
if (!userLocation || !playerStats || !playerStats.homeBaseLat) return null;
const metersPerDegLat = 111320;
const metersPerDegLng = 111320 * Math.cos(userLocation.lat * Math.PI / 180);
const dx = (userLocation.lng - playerStats.homeBaseLng) * metersPerDegLng;
const dy = (userLocation.lat - playerStats.homeBaseLat) * metersPerDegLat;
return Math.sqrt(dx * dx + dy * dy);
}
// Check if player has reached home base for respawn
function checkHomeBaseRespawn() {
if (!playerStats || !playerStats.isDead) return;
if (!playerStats.homeBaseLat) return;
const distance = getDistanceToHome();
if (distance === null) return;
// Update distance display
const distanceText = distance < 1000
? `${Math.round(distance)}m to home`
: `${(distance / 1000).toFixed(1)}km to home`;
document.getElementById('homeDistanceText').textContent = distanceText;
// Respawn if within home base radius
if (distance <= HOME_BASE_RADIUS) {
respawnPlayer();
}
}
// Check for HP/MP regeneration at home base
function checkHomeBaseRegen() {
// Skip if dead, no home base, or no player stats
if (!playerStats || playerStats.isDead) return;
if (!playerStats.homeBaseLat) return;
// Check if at full HP and MP already
if (playerStats.hp >= playerStats.maxHp && playerStats.mp >= playerStats.maxMp) return;
// Check distance to home
const distance = getDistanceToHome();
if (distance === null || distance > HOME_BASE_RADIUS) return;
// Check if enough time has passed since last regen
const now = Date.now();
if (now - lastHomeRegenTime < HOME_REGEN_INTERVAL) return;
// Regenerate HP and MP
let regenOccurred = false;
const hpRegen = Math.ceil(playerStats.maxHp * (HOME_REGEN_PERCENT / 100));
const mpRegen = Math.ceil(playerStats.maxMp * (HOME_REGEN_PERCENT / 100));
if (playerStats.hp < playerStats.maxHp) {
playerStats.hp = Math.min(playerStats.maxHp, playerStats.hp + hpRegen);
regenOccurred = true;
}
if (playerStats.mp < playerStats.maxMp) {
playerStats.mp = Math.min(playerStats.maxMp, playerStats.mp + mpRegen);
regenOccurred = true;
}
if (regenOccurred) {
lastHomeRegenTime = now;
updateRpgHud();
savePlayerStats();
// Show subtle regen indicator
showHomeRegenEffect();
}
}
// Show a subtle visual effect for home regen
function showHomeRegenEffect() {
const hud = document.getElementById('rpgHud');
if (!hud) return;
// Add a brief glow effect
hud.style.boxShadow = '0 0 15px rgba(76, 175, 80, 0.6)';
setTimeout(() => {
hud.style.boxShadow = '';
}, 500);
}
// Handle player death
async function handlePlayerDeath() {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/death', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
xpLostOnDeath = data.xpLost;
playerStats.xp = data.newXp;
playerStats.hp = 0;
playerStats.isDead = true;
// Clear local monster entourage
monsterEntourage.forEach(m => {
if (m.marker) map.removeLayer(m.marker);
});
monsterEntourage = [];
// Show death overlay
document.getElementById('xpLostText').textContent = `-${xpLostOnDeath} XP`;
document.getElementById('deathOverlay').style.display = 'flex';
// Update HUD
updateRpgHud();
console.log('Player died, lost', xpLostOnDeath, 'XP');
}
} catch (err) {
console.error('Failed to handle death:', err);
}
}
// Respawn player at home base
async function respawnPlayer() {
const token = localStorage.getItem('accessToken');
if (!token) return;
try {
const response = await fetch('/api/user/respawn', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
playerStats.isDead = false;
playerStats.hp = data.hp;
playerStats.mp = data.mp;
// Hide death overlay
document.getElementById('deathOverlay').style.display = 'none';
// Update HUD
updateRpgHud();
savePlayerStats();
// Spawn a monster after a short delay
setTimeout(() => {
spawnMonsterNearPlayer();
}, 2000);
console.log('Player respawned with full HP/MP');
}
} catch (err) {
console.error('Failed to respawn:', err);
}
} }
// Save monsters to server (debounced) // Save monsters to server (debounced)
@ -9865,6 +10656,12 @@
monsterEntourage.push(monster); monsterEntourage.push(monster);
} }
updateRpgHud(); updateRpgHud();
// Set last spawn location so player needs to move before more spawn
if (userLocation) {
lastSpawnLocation = { lat: userLocation.lat, lng: userLocation.lng };
}
return true; return true;
} }
} }
@ -9882,12 +10679,13 @@
// First, try to load existing monsters // First, try to load existing monsters
await loadMonsters(); await loadMonsters();
// Spawn check every 20 seconds
// Spawn check based on settings (interval and chance)
monsterSpawnTimer = setInterval(() => { monsterSpawnTimer = setInterval(() => {
if (Math.random() < 0.5) { // 50% chance each interval
const chanceRoll = Math.random() * 100;
if (chanceRoll < spawnSettings.spawnChance) {
spawnMonsterNearPlayer(); spawnMonsterNearPlayer();
} }
}, 20000);
}, spawnSettings.spawnInterval);
// Update monster positions and dialogue every 2 seconds // Update monster positions and dialogue every 2 seconds
monsterUpdateTimer = setInterval(() => { monsterUpdateTimer = setInterval(() => {
@ -9917,9 +10715,25 @@
// Spawn a monster near the player // Spawn a monster near the player
function spawnMonsterNearPlayer() { function spawnMonsterNearPlayer() {
if (!userLocation || !playerStats) return; if (!userLocation || !playerStats) return;
if (playerStats.isDead) return; // Don't spawn when dead
if (monsterEntourage.length >= getMaxMonsters()) return; if (monsterEntourage.length >= getMaxMonsters()) return;
if (!monsterTypesLoaded || Object.keys(MONSTER_TYPES).length === 0) return; if (!monsterTypesLoaded || Object.keys(MONSTER_TYPES).length === 0) return;
// Movement-based spawning: first monster can spawn standing still,
// but subsequent monsters require player to move the configured distance
if (monsterEntourage.length > 0 && lastSpawnLocation) {
const metersPerDegLat = 111320;
const metersPerDegLng = 111320 * Math.cos(userLocation.lat * Math.PI / 180);
const dx = (userLocation.lng - lastSpawnLocation.lng) * metersPerDegLng;
const dy = (userLocation.lat - lastSpawnLocation.lat) * metersPerDegLat;
const distanceMoved = Math.sqrt(dx * dx + dy * dy);
if (distanceMoved < spawnSettings.spawnDistance) {
// Player hasn't moved far enough yet, skip spawn
return;
}
}
// Pick a random monster type from available types // Pick a random monster type from available types
const typeIds = Object.keys(MONSTER_TYPES); const typeIds = Object.keys(MONSTER_TYPES);
const typeId = typeIds[Math.floor(Math.random() * typeIds.length)]; const typeId = typeIds[Math.floor(Math.random() * typeIds.length)];
@ -9936,7 +10750,11 @@
const offsetLat = (distance * Math.cos(angle)) / metersPerDegLat; const offsetLat = (distance * Math.cos(angle)) / metersPerDegLat;
const offsetLng = (distance * Math.sin(angle)) / metersPerDegLng; const offsetLng = (distance * Math.sin(angle)) / metersPerDegLng;
const monsterLevel = Math.max(1, playerStats.level + Math.floor(Math.random() * 3) - 1);
// Calculate monster level based on player level, but respect monster type's min/max level
const baseLevel = Math.max(1, playerStats.level + Math.floor(Math.random() * 3) - 1);
const minLevel = monsterType.minLevel || 1;
const maxLevel = monsterType.maxLevel || 99;
const monsterLevel = Math.max(minLevel, Math.min(maxLevel, baseLevel));
const monster = { const monster = {
id: `monster_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, id: `monster_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
@ -9960,6 +10778,9 @@
updateRpgHud(); updateRpgHud();
saveMonsters(); // Persist to server saveMonsters(); // Persist to server
// Update last spawn location for movement-based spawning
lastSpawnLocation = { lat: userLocation.lat, lng: userLocation.lng };
console.log('Spawned monster:', monster.id, 'at level', monsterLevel); console.log('Spawned monster:', monster.id, 'at level', monsterLevel);
} }
@ -10091,6 +10912,7 @@
if (combatState) return; // Already in combat if (combatState) return; // Already in combat
if (!playerStats) return; if (!playerStats) return;
if (monsterEntourage.length === 0) return; if (monsterEntourage.length === 0) return;
if (playerStats.isDead) return; // Can't fight when dead
// Load skills for each unique monster type // Load skills for each unique monster type
const uniqueTypes = [...new Set(monsterEntourage.map(m => m.type))]; const uniqueTypes = [...new Set(monsterEntourage.map(m => m.type))];
@ -10775,9 +11597,21 @@
// Handle combat defeat // Handle combat defeat
function handleCombatDefeat() { function handleCombatDefeat() {
const monsterCount = combatState.monsters.filter(m => m.hp > 0).length; const monsterCount = combatState.monsters.filter(m => m.hp > 0).length;
// If player has a home base, trigger death system
if (playerStats.homeBaseLat && playerStats.homeBaseLng) {
addCombatLog(`💀 You have been slain! Return to your home base to respawn.`, 'damage');
playerStats.mp = combatState.player.mp;
// Close combat first, then handle death
setTimeout(() => {
closeCombatUI();
handlePlayerDeath();
}, 1500);
} else {
// No home base - use old behavior (restore HP to 50%)
addCombatLog(`You were defeated! ${monsterCount} ${monsterCount === 1 ? 'enemy remains' : 'enemies remain'}. HP restored to 50%.`, 'damage'); addCombatLog(`You were defeated! ${monsterCount} ${monsterCount === 1 ? 'enemy remains' : 'enemies remain'}. HP restored to 50%.`, 'damage');
// Restore HP to 50%
playerStats.hp = Math.floor(playerStats.maxHp * 0.5); playerStats.hp = Math.floor(playerStats.maxHp * 0.5);
playerStats.mp = combatState.player.mp; playerStats.mp = combatState.player.mp;
@ -10786,6 +11620,7 @@
setTimeout(closeCombatUI, 2500); setTimeout(closeCombatUI, 2500);
} }
}
// Flee from combat // Flee from combat
function fleeCombat() { function fleeCombat() {
@ -10840,8 +11675,8 @@
// END RPG COMBAT SYSTEM FUNCTIONS // END RPG COMBAT SYSTEM FUNCTIONS
// ========================================== // ==========================================
// Load monster types and skills from database, then initialize auth
Promise.all([loadMonsterTypes(), loadSkillsFromDatabase()]).then(() => {
// Load monster types, skills, and spawn settings from database, then initialize auth
Promise.all([loadMonsterTypes(), loadSkillsFromDatabase(), loadSpawnSettings()]).then(() => {
loadCurrentUser(); loadCurrentUser();
}); });

154
server.js

@ -777,7 +777,12 @@ app.get('/api/user/rpg-stats', authenticateToken, (req, res) => {
def: stats.def, def: stats.def,
accuracy: stats.accuracy || 90, accuracy: stats.accuracy || 90,
dodge: stats.dodge || 10, dodge: stats.dodge || 10,
unlockedSkills: unlockedSkills
unlockedSkills: unlockedSkills,
homeBaseLat: stats.home_base_lat,
homeBaseLng: stats.home_base_lng,
lastHomeSet: stats.last_home_set,
isDead: !!stats.is_dead,
homeBaseIcon: stats.home_base_icon || '00'
}); });
} else { } else {
// No stats yet - return null so client creates defaults // No stats yet - return null so client creates defaults
@ -849,6 +854,150 @@ app.put('/api/user/rpg-stats', authenticateToken, (req, res) => {
} }
}); });
// Check if user can set home base (once per day)
app.get('/api/user/can-set-home', authenticateToken, (req, res) => {
try {
const canSet = db.canSetHomeBase(req.user.userId);
res.json({ canSet });
} catch (err) {
console.error('Check home base error:', err);
res.status(500).json({ error: 'Failed to check home base availability' });
}
});
// Set home base location
app.post('/api/user/home-base', authenticateToken, (req, res) => {
try {
const { lat, lng } = req.body;
if (lat === undefined || lng === undefined) {
return res.status(400).json({ error: 'Latitude and longitude are required' });
}
// Check if user can set home base (once per day)
if (!db.canSetHomeBase(req.user.userId)) {
return res.status(400).json({ error: 'You can only set your home base once per day' });
}
db.setHomeBase(req.user.userId, lat, lng);
res.json({ success: true, homeBaseLat: lat, homeBaseLng: lng });
} catch (err) {
console.error('Set home base error:', err);
res.status(500).json({ error: 'Failed to set home base' });
}
});
// Get available homebase icons (auto-detected from mapgameimgs directory)
app.get('/api/homebase-icons', (req, res) => {
try {
const fs = require('fs');
const imagesDir = path.join(__dirname, 'mapgameimgs');
// Read directory and find homebaseXX-100.png files
const files = fs.readdirSync(imagesDir);
const iconPattern = /^homebase(\d+)-100\.png$/;
const icons = files
.map(file => {
const match = file.match(iconPattern);
if (match) {
return {
id: match[1],
filename: file,
preview: `/mapgameimgs/${file}`, // Use 100px, CSS scales down
full: `/mapgameimgs/${file}`
};
}
return null;
})
.filter(Boolean)
.sort((a, b) => a.id.localeCompare(b.id));
res.json(icons);
} catch (err) {
console.error('Get homebase icons error:', err);
res.status(500).json({ error: 'Failed to get homebase icons' });
}
});
// Get spawn settings (public - client needs these for spawn logic)
app.get('/api/spawn-settings', (req, res) => {
try {
const settings = {
spawnInterval: JSON.parse(db.getSetting('monsterSpawnInterval') || '20000'),
spawnChance: JSON.parse(db.getSetting('monsterSpawnChance') || '50'),
spawnDistance: JSON.parse(db.getSetting('monsterSpawnDistance') || '10')
};
res.json(settings);
} catch (err) {
console.error('Get spawn settings error:', err);
res.status(500).json({ error: 'Failed to get spawn settings' });
}
});
// Update home base icon
app.put('/api/user/home-base/icon', authenticateToken, (req, res) => {
try {
const { iconId } = req.body;
if (!iconId) {
return res.status(400).json({ error: 'Icon ID is required' });
}
db.setHomeBaseIcon(req.user.userId, iconId);
res.json({ success: true, homeBaseIcon: iconId });
} catch (err) {
console.error('Set home base icon error:', err);
res.status(500).json({ error: 'Failed to set home base icon' });
}
});
// Handle player death
app.post('/api/user/death', authenticateToken, (req, res) => {
try {
const result = db.handlePlayerDeath(req.user.userId, 10); // 10% XP penalty
if (!result) {
return res.status(404).json({ error: 'Player stats not found' });
}
// Clear monster entourage
db.clearMonsterEntourage(req.user.userId);
res.json({
success: true,
xpLost: result.xpLost,
newXp: result.newXp
});
} catch (err) {
console.error('Handle death error:', err);
res.status(500).json({ error: 'Failed to handle death' });
}
});
// Respawn player at home base
app.post('/api/user/respawn', authenticateToken, (req, res) => {
try {
const stats = db.getRpgStats(req.user.userId);
if (!stats) {
return res.status(404).json({ error: 'Player stats not found' });
}
if (!stats.is_dead) {
return res.status(400).json({ error: 'Player is not dead' });
}
db.respawnPlayer(req.user.userId);
res.json({
success: true,
hp: stats.max_hp,
mp: stats.max_mp
});
} catch (err) {
console.error('Respawn error:', err);
res.status(500).json({ error: 'Failed to respawn' });
}
});
// Get all monster types (public endpoint - needed for game rendering) // Get all monster types (public endpoint - needed for game rendering)
app.get('/api/monster-types', (req, res) => { app.get('/api/monster-types', (req, res) => {
try { try {
@ -864,6 +1013,9 @@ app.get('/api/monster-types', (req, res) => {
xpReward: t.xp_reward, xpReward: t.xp_reward,
accuracy: t.accuracy || 85, accuracy: t.accuracy || 85,
dodge: t.dodge || 5, dodge: t.dodge || 5,
minLevel: t.min_level || 1,
maxLevel: t.max_level || 99,
spawnWeight: t.spawn_weight || 100,
levelScale: { levelScale: {
hp: t.level_scale_hp, hp: t.level_scale_hp,
atk: t.level_scale_atk, atk: t.level_scale_atk,

39
to_do.md

@ -45,15 +45,46 @@
- [ ] Add equipment UI to character sheet - [ ] Add equipment UI to character sheet
- [ ] Calculate effective stats with equipment bonuses - [ ] Calculate effective stats with equipment bonuses
## Phase 6: Admin Editor
- [ ] Create admin.html (separate page)
- [ ] Add admin authentication middleware
## Phase 6: Admin Editor - MOSTLY COMPLETE
- [x] Create admin.html (separate page)
- [x] Add admin authentication middleware
- [ ] User management (list, edit stats, grant admin) - [ ] User management (list, edit stats, grant admin)
- [x] Monster types stored in database (monster_types table created) - [x] Monster types stored in database (monster_types table created)
- [ ] Monster management UI (CRUD for monster_types)
- [x] Monster management UI (CRUD for monster_types)
- [x] Monster cloning
- [x] Monster enable/disable toggle
- [x] Auto-copy default images for new monsters
- [ ] Spawn control (manual monster spawning) - [ ] Spawn control (manual monster spawning)
- [ ] Game balance settings - [ ] Game balance settings
## Phase 8: Home Base / Death System - COMPLETE
- [x] Add home_base_lat, home_base_lng, last_home_set, is_dead columns to rpg_stats
- [x] Add "Set Home Base" mode - tap on map to select location
- [x] Limit home base setting to once per day (check last_home_set timestamp)
- [x] Show home base marker on map (uses default50.png)
- [x] On death:
- [x] Set player state to "dead" (can't initiate combat)
- [x] All monsters despawn (clear entourage)
- [x] Lose 10% of XP (but cannot drop below current level threshold)
- [x] Show "You are dead! Return to home base to respawn" message
- [x] Visual indicator when dead (grayed HUD with skull icon)
- [x] Detect when dead player reaches home base (~20m radius)
- [x] Respawn player with full HP and MP when they reach home
- [x] If no home base set, old behavior (restore 50% HP on defeat)
## Phase 7: Skill Database System - COMPLETE
- [x] Skills table in database
- [x] Skills admin page (CRUD)
- [x] Hit/miss mechanics (accuracy vs dodge)
- [x] Monster skills with weighted random selection
- [x] Custom skill names per monster
- [x] Status effects (poison) with turn-based damage
- [x] Buff skills (defend) working properly
- [x] Status effect visual overlays (100x100px)
- [x] Monster min/max level spawning
- [ ] Class-specific skill names (getSkillForClass)
- [ ] Class skill names admin editor
--- ---
## Completed ## Completed

Loading…
Cancel
Save