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. 871
      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

871
index.html
File diff suppressed because it is too large
View File

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