Browse Source

Simplify fog of war, fix combat timing, update docs

- Fog of war now uses single hard edge instead of multi-layer gradient
- Monster attack animations now play before damage is applied (500ms sync)
- Update CLAUDE.md and README.md to document MapLibre (not Leaflet)
- Fix service-worker.js to cache MapLibre instead of Leaflet
- Add documentation for fog of war, virtual movement, winch system
- Add karen_base monster sprites
- Various animation and admin panel improvements

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
master
HikeMap User 3 weeks ago
parent
commit
2a07c89e1c
  1. 87
      CLAUDE.md
  2. 18
      README.md
  3. 330
      admin.html
  4. 100
      animations.js
  5. 32
      artwork_todo.md
  6. 78
      database.js
  7. 272
      geocaches.json
  8. 2072
      index.html
  9. BIN
      mapgameimgs/monsters/karen_base100.png
  10. BIN
      mapgameimgs/monsters/karen_base50.png
  11. BIN
      mapgameimgs/skills/full_restore.png
  12. BIN
      mapgamemusic/homebase.mp3
  13. 4
      server.js
  14. 7
      service-worker.js

87
CLAUDE.md

@ -29,13 +29,25 @@ Then open http://localhost:8080
| `index.html` | Frontend SPA (CSS, HTML, JavaScript) |
| `server.js` | Express API server, WebSocket handling |
| `database.js` | SQLite database layer (better-sqlite3) |
| `animations.js` | Combat/monster animation definitions |
| `docker-compose.yml` | Container configuration |
| `hikemap.db` | SQLite database (auto-created) |
| `SKILLS.md` | Complete skills documentation |
### Asset Directories
| Directory | Purpose |
|-----------|---------|
| `mapgameimgs/monsters/` | Monster sprites (50px and 100px versions) |
| `mapgameimgs/bases/` | Home base icons |
| `mapgameimgs/skills/` | Custom skill icons (uploaded via admin) |
| `mapgameimgs/cacheicons/` | Geocache type icons |
| `mapgamemusic/` | Background music tracks |
| `sfx/` | Sound effects (combat, spawns, etc.) |
### Key Libraries
- **Frontend**: Leaflet.js 1.9.4, leaflet-rotate 0.2.8
- **Frontend**: MapLibre GL JS 4.1.0 (WebGL vector maps), Turf.js (geospatial math)
- **Backend**: Express, better-sqlite3, jsonwebtoken, ws (WebSocket)
- **Note**: Codebase has Leaflet compatibility shims for gradual migration
---
@ -63,12 +75,27 @@ Then open http://localhost:8080
- Required to respawn after death
- Skill loadout can only be changed at home
**Fog of War**:
- Canvas-based overlay that hides unexplored areas
- Two reveal zones: homebase radius and player explore radius
- Geocaches only visible/interactable in revealed areas
- Multipliers can modify reveal radius (via utility skills)
- Hard-edged cutout (no gradient layers)
**Virtual Movement Mode**:
- Developer/testing feature for moving without real GPS
- WASD/arrow keys move virtual position on map
- Winch tethering system: walking away from virtual position reels it back
- Prevents unlimited virtual exploration - real GPS movement matters
- Toggle via developer tools panel
### Combat System
**Monster Spawning**:
- Monsters spawn while walking (configurable distance)
- No spawns within home base radius
- Multiple monsters can accumulate (entourage)
- Spawn sound effect plays when monster appears (`sfx/` directory)
**Combat Flow**:
1. Player taps monster to engage
@ -83,6 +110,14 @@ damage = (skillPower * playerATK / 100) - enemyDEF
hitChance = skillAccuracy + (attackerAccuracy - 90) - defenderDodge
```
**Animation System** (`animations.js`):
- Separate file defines all combat animations
- Player animations: `attack`, `skill`, `miss`, `death`
- Monster animations: `attack`, `skill`, `miss`, `death`, `idle`, `bouncy`, various flips
- Each animation has: `duration`, `loop`, `easing`, `keyframes`
- Combat timing syncs damage with animation "hit" moment (500ms delay)
- Skills can override default animation via `animation` field
### Skill System
**Skill Tiers**: Skills unlock at level milestones (2, 3, 4, 5, 6, 7)
@ -102,6 +137,12 @@ hitChance = skillAccuracy + (attackerAccuracy - 90) - defenderDodge
See `SKILLS.md` for complete skill reference.
**Custom Skill Icons**:
- Upload via admin panel (`/admin.html`)
- Stored in `mapgameimgs/skills/` directory
- Database field: `skills.icon` stores filename
- Falls back to emoji if no custom icon set
### Classes
**Trail Runner** (currently the only class):
@ -221,21 +262,53 @@ db.swapActiveSkill(userId, tier, newSkillId) // Swap loadout
### Key State Variables
```javascript
// RPG State
playerStats // Player RPG data
combatState // Active combat info
monsterEntourage // Spawned monsters
statsLoadedFromServer // Prevents stale saves
pendingSkillChoice // Level-up skill selection
// GPS & Virtual Movement
userLocation // Current position (real or virtual)
realGpsPosition // Actual GPS coordinates
testPosition // Virtual position when in test mode
gpsTestMode // Virtual movement enabled flag
previousWinchRealGps // Winch system - tracks real GPS for tethering
// Fog of War
fogCanvas // Canvas element for fog overlay
fogCtx // 2D context for fog drawing
fogSystemReady // Flag indicating fog can render
exploreRadiusMultiplier // Skill-modified explore radius
homebaseRadiusMultiplier // Skill-modified homebase radius
```
### Key Functions
```javascript
// RPG & Combat
initializePlayerStats(username) // Load from server
savePlayerStats() // Save to server
showSkillChoiceModal(level) // Level-up skill pick
swapSkillFromHomebase(tier, id) // Change loadout
startCombat(monsters) // Begin combat
useSkill(skillId) // Execute skill in combat
// Fog of War
updateFogOfWar() // Redraw fog overlay
isInRevealedArea(lat, lng) // Check if location visible
initFogOfWar() // Create fog canvas
// Virtual Movement & Winch
enableVirtualMovement() // Enter virtual GPS mode
disableVirtualMovement() // Return to real GPS
simulateGpsPosition() // Update map with virtual position
applyWinchTether(newRealPosition) // Reel in virtual when walking away
// Animations (from animations.js)
getAnimation(animationId) // Get animation definition
getAnimationList() // List monster animations
getPlayerAnimationList() // List player animations
```
---
@ -284,16 +357,18 @@ Most source files are **volume-mounted** in `docker-compose.yml`, so changes are
| File | Action | Notes |
|------|--------|-------|
| `index.html` | Browser refresh | Volume-mounted, served fresh |
| `admin.html` | Browser refresh | Volume-mounted, served fresh |
| `service-worker.js` | Browser refresh + bump cache version | Volume-mounted |
| `server.js` | `docker restart hikemap_hikemap_1` | Volume-mounted, Node needs restart |
| `database.js` | `docker restart hikemap_hikemap_1` | Volume-mounted, Node needs restart |
| `index.html` | **Rebuild container** | Volume mount may not sync live |
| `admin.html` | **Rebuild container** | Volume mount may not sync live |
| `service-worker.js` | **Rebuild container** + bump cache version | Volume mount may not sync live |
| `server.js` | **Rebuild container** | Volume mount may not sync live |
| `database.js` | **Rebuild container** | Volume mount may not sync live |
| `mapgameimgs/*` | Browser refresh | Volume-mounted |
| `mapgamemusic/*` | Browser refresh | Volume-mounted |
| `sfx/*` | Browser refresh | Volume-mounted |
| `hikemap.db` | Nothing | Volume-mounted, persists outside container |
**IMPORTANT**: Despite being volume-mounted, code files (`index.html`, `server.js`, etc.) often require a container rebuild to reflect changes. Always rebuild when in doubt.
**Files requiring full rebuild** (`docker-compose up -d --build`):
| File | Why Rebuild? |

18
README.md

@ -15,6 +15,7 @@ A location-based RPG web application where you walk in the real world to explore
- Turn-based battles against MOOP (Matter Out Of Place) monsters
- Skill-based combat with damage, healing, buffs, and multi-hit attacks
- Hit/miss mechanics based on accuracy and dodge stats
- Animated combat with synchronized damage timing
- XP rewards and leveling system
**Skills & Progression**
@ -29,6 +30,12 @@ A location-based RPG web application where you walk in the real world to explore
- Required destination when defeated
- Skill loadout management location
**Fog of War**
- Unexplored areas hidden by dark overlay
- Reveal zones around your home base and current position
- Geocaches only visible in revealed areas
- Utility skills can expand your reveal radius
### Track Editing
- Draw new GPS tracks directly on the map
@ -106,11 +113,18 @@ See [SKILLS.md](SKILLS.md) for complete skill details.
- **Frontend**: Single-page app in `index.html`
- **Backend**: Node.js/Express server in `server.js`
- **Database**: SQLite via better-sqlite3 in `database.js`
- **Animations**: Combat animation definitions in `animations.js`
- **Real-time**: WebSocket for live updates
### Developer Features
- Virtual movement mode for testing without real GPS
- Winch tethering system prevents unlimited virtual exploration
- Admin panel for monster/skill management and icon uploads
- Customizable spawn settings and game balance
### Dependencies
- Leaflet.js 1.9.4 (maps)
- leaflet-rotate 0.2.8 (navigation rotation)
- MapLibre GL JS 4.1.0 (WebGL vector maps)
- Turf.js (geospatial calculations)
- Express (API server)
- better-sqlite3 (database)
- jsonwebtoken (authentication)

330
admin.html

@ -580,15 +580,102 @@
}
.monster-skill-item .skill-animation {
width: 100px;
width: 90px;
padding: 4px;
font-size: 12px;
font-size: 11px;
background: rgba(255,255,255,0.1);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
color: inherit;
}
.skill-animation-section {
display: flex;
flex-direction: column;
gap: 2px;
}
.skill-animation-row {
display: flex;
gap: 4px;
align-items: center;
}
.skill-animation-row select {
flex: 1;
}
.anim-test-btn {
width: 24px;
height: 24px;
padding: 0;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 4px;
background: rgba(100,200,100,0.2);
color: #8f8;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.anim-test-btn:hover {
background: rgba(100,200,100,0.4);
}
/* Animation tester panel */
.animation-tester {
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.animation-tester h4 {
margin: 0 0 10px 0;
color: #8cf;
}
.animation-tester-content {
display: flex;
gap: 20px;
align-items: center;
}
.animation-preview {
width: 64px;
height: 64px;
background: rgba(255,255,255,0.1);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.animation-preview img {
width: 48px;
height: 48px;
object-fit: contain;
}
.animation-tester-controls {
display: flex;
flex-direction: column;
gap: 8px;
}
.animation-tester-controls select {
padding: 6px 10px;
min-width: 150px;
}
.animation-tester-controls button {
padding: 6px 15px;
}
.monster-skill-item label {
font-size: 11px;
color: #888;
@ -1322,6 +1409,12 @@
<!-- Populated dynamically -->
</select>
</div>
<div class="form-group">
<label>Miss Animation</label>
<select id="monsterMissAnim">
<!-- Populated dynamically -->
</select>
</div>
</div>
<h4>Animation Preview</h4>
@ -1506,6 +1599,33 @@
</div>
</div>
<!-- Combat Buff Configuration (hidden by default, shown for buff type) -->
<div class="status-effect-section" id="buffConfigSection" style="display: none;">
<h4>Combat Buff Configuration</h4>
<p style="font-size: 11px; color: #666; margin-bottom: 10px;">
Configure the in-combat buff effect (e.g., Defense Up, Accuracy Up)
</p>
<div class="form-row-3">
<div class="form-group">
<label>Buff Type</label>
<select id="buffEffectType">
<option value="defense_up">Defense Up</option>
<option value="accuracy_up">Accuracy Up</option>
<option value="attack_up">Attack Up</option>
<option value="dodge_up">Dodge Up</option>
</select>
</div>
<div class="form-group">
<label>Boost (%)</label>
<input type="number" id="buffEffectPercent" value="20" min="1" max="200">
</div>
<div class="form-group">
<label>Duration (turns)</label>
<input type="number" id="buffEffectDuration" value="3" min="1" max="10">
</div>
</div>
</div>
<!-- Utility Skill Configuration (hidden by default) -->
<div class="status-effect-section" id="utilityConfigSection" style="display: none;">
<h4>Utility Skill Configuration</h4>
@ -1525,6 +1645,7 @@
<option value="xp_multiplier">XP Multiplier</option>
<option value="explore_radius_multiplier">Explore Radius Multiplier</option>
<option value="homebase_radius_multiplier">Homebase Radius Multiplier</option>
<option value="wander_range_multiplier">Wander Range Multiplier</option>
</select>
</div>
<div class="form-group">
@ -1672,6 +1793,23 @@
Assign skills to this class. Set unlock level and choice group for skill selection at level-up.
<br>Skills with the same choice group at the same level = player picks one.
</p>
<!-- Animation Tester -->
<div class="animation-tester">
<h4>🎬 Animation Tester</h4>
<div class="animation-tester-content">
<div class="animation-preview" id="animationPreview">
<img src="/mapgameimgs/player/runner.png" id="animationPreviewImg">
</div>
<div class="animation-tester-controls">
<select id="animationTesterSelect">
<option value="">-- Select Animation --</option>
</select>
<button type="button" class="btn btn-primary" onclick="testAnimation()">▶ Play</button>
</div>
</div>
</div>
<div id="classSkillsList"></div>
<div class="add-skill-row">
<select id="addClassSkillSelect">
@ -2111,6 +2249,7 @@
document.getElementById('monsterAttackAnim').value = monster.attack_animation || 'attack';
document.getElementById('monsterDeathAnim').value = monster.death_animation || 'death';
document.getElementById('monsterIdleAnim').value = monster.idle_animation || 'idle';
document.getElementById('monsterMissAnim').value = monster.miss_animation || 'miss';
// Update preview icon
document.getElementById('animPreviewIcon').src = `/mapgameimgs/monsters/${monster.key}100.png`;
@ -2164,6 +2303,7 @@
document.getElementById('monsterAttackAnim').value = monster.attack_animation || 'attack';
document.getElementById('monsterDeathAnim').value = monster.death_animation || 'death';
document.getElementById('monsterIdleAnim').value = monster.idle_animation || 'idle';
document.getElementById('monsterMissAnim').value = monster.miss_animation || 'miss';
document.getElementById('animPreviewIcon').src = '/mapgameimgs/monsters/default100.png';
// Clear skills (cloned monster needs to be saved first)
@ -2183,6 +2323,7 @@
document.getElementById('monsterAttackAnim').value = 'attack';
document.getElementById('monsterDeathAnim').value = 'death';
document.getElementById('monsterIdleAnim').value = 'idle';
document.getElementById('monsterMissAnim').value = 'miss';
document.getElementById('animPreviewIcon').src = '/mapgameimgs/monsters/default100.png';
// Clear skills (new monster needs to be saved first)
currentMonsterSkills = [];
@ -2199,7 +2340,7 @@
const animations = typeof MONSTER_ANIMATIONS !== 'undefined' ? MONSTER_ANIMATIONS : {};
const animIds = Object.keys(animations);
const dropdowns = ['monsterAttackAnim', 'monsterDeathAnim', 'monsterIdleAnim', 'testAnimationSelect'];
const dropdowns = ['monsterAttackAnim', 'monsterDeathAnim', 'monsterIdleAnim', 'monsterMissAnim', 'testAnimationSelect'];
dropdowns.forEach(dropdownId => {
const dropdown = document.getElementById(dropdownId);
@ -2227,6 +2368,119 @@
}).join('');
}
// Get player animation options HTML (for class skill dropdowns)
function getPlayerAnimationOptionsHtml(selectedValue) {
let options = '<option value="">Default</option>';
// Add player animations first (if available)
if (typeof PLAYER_ANIMATIONS !== 'undefined') {
options += '<optgroup label="Player Animations">';
for (const [id, anim] of Object.entries(PLAYER_ANIMATIONS)) {
const selected = selectedValue === id ? 'selected' : '';
options += `<option value="${id}" ${selected}>${anim.name}</option>`;
}
options += '</optgroup>';
}
// Add monster animations (player can use these too)
if (typeof MONSTER_ANIMATIONS !== 'undefined') {
options += '<optgroup label="Monster Animations">';
for (const [id, anim] of Object.entries(MONSTER_ANIMATIONS)) {
const selected = selectedValue === id ? 'selected' : '';
options += `<option value="${id}" ${selected}>${anim.name}</option>`;
}
options += '</optgroup>';
}
return options;
}
// Initialize animation tester dropdown
function initAnimationTester() {
const select = document.getElementById('animationTesterSelect');
if (!select) return;
select.innerHTML = getPlayerAnimationOptionsHtml('');
}
// Test animation from the main tester panel
function testAnimation() {
const select = document.getElementById('animationTesterSelect');
const animationId = select.value;
if (!animationId) {
showToast('Select an animation first', 'error');
return;
}
playAnimationPreview(animationId, 'animationPreviewImg');
}
// Test animation for a specific skill
function testSkillAnimation(classSkillId) {
const select = document.getElementById(`anim-select-${classSkillId}`);
let animationId = select ? select.value : '';
// If no animation selected, use default attack
if (!animationId) {
animationId = 'attack';
}
playAnimationPreview(animationId, 'animationPreviewImg');
}
// Play animation on the preview element
function playAnimationPreview(animationId, imgElementId) {
const img = document.getElementById(imgElementId);
if (!img) return;
// Get animation definition
let anim = null;
let prefix = 'player';
if (typeof PLAYER_ANIMATIONS !== 'undefined' && PLAYER_ANIMATIONS[animationId]) {
anim = PLAYER_ANIMATIONS[animationId];
prefix = 'player';
} else if (typeof MONSTER_ANIMATIONS !== 'undefined' && MONSTER_ANIMATIONS[animationId]) {
anim = MONSTER_ANIMATIONS[animationId];
prefix = 'monster';
}
if (!anim) {
showToast('Animation not found', 'error');
return;
}
// Inject keyframes if not already present
injectAnimationKeyframes(animationId, anim, prefix);
// Reset and play animation
const loopStr = anim.loop ? ' infinite' : '';
const fillStr = anim.fillMode ? ` ${anim.fillMode}` : '';
const easing = anim.easing || 'ease-out';
img.style.animation = 'none';
img.offsetHeight; // Force reflow
img.style.animation = `${prefix}_${animationId} ${anim.duration}ms ${easing}${loopStr}${fillStr}`;
// Remove animation after it completes (unless looping)
if (!anim.loop) {
setTimeout(() => {
img.style.animation = '';
}, anim.duration);
}
}
// Inject CSS keyframes for an animation if not already present
function injectAnimationKeyframes(animationId, anim, prefix) {
const animName = `${prefix}_${animationId}`;
// Check if already injected
if (document.getElementById(`keyframe-${animName}`)) return;
const style = document.createElement('style');
style.id = `keyframe-${animName}`;
style.textContent = `@keyframes ${animName} { ${anim.keyframes} }`;
document.head.appendChild(style);
}
// Update artwork preview in OSM tag modal
function updateArtworkPreview() {
const artworkNum = parseInt(document.getElementById('osmTagArtwork').value) || 1;
@ -2322,6 +2576,7 @@
attack_animation: document.getElementById('monsterAttackAnim').value,
death_animation: document.getElementById('monsterDeathAnim').value,
idle_animation: document.getElementById('monsterIdleAnim').value,
miss_animation: document.getElementById('monsterMissAnim').value,
dialogues: JSON.stringify(dialogues)
};
@ -2424,7 +2679,8 @@
'def_boost_percent': 'DEF %',
'xp_multiplier': 'XP',
'explore_radius_multiplier': 'Explore Radius',
'homebase_radius_multiplier': 'Homebase Radius'
'homebase_radius_multiplier': 'Homebase Radius',
'wander_range_multiplier': 'Wander Range'
};
tbody.innerHTML = utilitySkills.map(s => {
@ -2465,19 +2721,29 @@
function handleSkillTypeChange() {
const skillType = document.getElementById('skillType').value;
const utilitySection = document.getElementById('utilityConfigSection');
const statusEffectSection = document.querySelector('.status-effect-section:not(#utilityConfigSection)');
const buffSection = document.getElementById('buffConfigSection');
const statusEffectSection = document.querySelector('.status-effect-section:not(#utilityConfigSection):not(#buffConfigSection)');
// Hide all special sections first
utilitySection.style.display = 'none';
buffSection.style.display = 'none';
if (statusEffectSection) statusEffectSection.style.display = 'none';
if (skillType === 'utility') {
utilitySection.style.display = 'block';
if (statusEffectSection) statusEffectSection.style.display = 'none';
// Auto-set defaults for utility skills
document.getElementById('skillPlayerUsable').checked = false;
document.getElementById('skillMonsterUsable').checked = false;
document.getElementById('skillTarget').value = 'self';
document.getElementById('skillMpCost').value = '0';
document.getElementById('skillBasePower').value = '0';
} else if (skillType === 'buff') {
buffSection.style.display = 'block';
// Auto-set defaults for buff skills
document.getElementById('skillTarget').value = 'self';
document.getElementById('skillBasePower').value = '0';
} else {
utilitySection.style.display = 'none';
// Show status effect section for damage/heal/status/debuff types
if (statusEffectSection) statusEffectSection.style.display = 'block';
}
}
@ -2543,31 +2809,51 @@
document.getElementById('utilityEffectValue').value = effect.effectValue || 2.0;
document.getElementById('utilityDurationHours').value = effect.durationHours || 1;
document.getElementById('utilityCooldownHours').value = effect.cooldownHours || 24;
// Reset combat status effect fields
// Reset other fields
document.getElementById('skillStatusType').value = '';
document.getElementById('buffEffectType').value = 'defense_up';
document.getElementById('buffEffectPercent').value = 20;
document.getElementById('buffEffectDuration').value = 3;
} else if (skill.type === 'buff') {
// Combat buff config (defense_up, accuracy_up, etc.)
document.getElementById('buffEffectType').value = effect.type || 'defense_up';
document.getElementById('buffEffectPercent').value = effect.percent || 20;
document.getElementById('buffEffectDuration').value = effect.duration || 3;
// Reset other fields
document.getElementById('skillStatusType').value = '';
document.getElementById('utilityEffectType').value = 'mp_regen_multiplier';
document.getElementById('utilityEffectValue').value = 2.0;
document.getElementById('utilityDurationHours').value = 1;
document.getElementById('utilityCooldownHours').value = 24;
} else {
// Combat status effect
// Combat status effect (poison, burn, etc.)
document.getElementById('skillStatusType').value = effect.type || '';
document.getElementById('skillStatusDamage').value = effect.damage || 5;
document.getElementById('skillStatusDuration').value = effect.duration || 3;
// Reset utility fields
// Reset other fields
document.getElementById('utilityEffectType').value = 'mp_regen_multiplier';
document.getElementById('utilityEffectValue').value = 2.0;
document.getElementById('utilityDurationHours').value = 1;
document.getElementById('utilityCooldownHours').value = 24;
document.getElementById('buffEffectType').value = 'defense_up';
document.getElementById('buffEffectPercent').value = 20;
document.getElementById('buffEffectDuration').value = 3;
}
} catch {
document.getElementById('skillStatusType').value = '';
}
} else {
// Reset all effect fields to defaults
document.getElementById('skillStatusType').value = '';
document.getElementById('skillStatusDamage').value = 5;
document.getElementById('skillStatusDuration').value = 3;
// Reset utility fields to defaults
document.getElementById('utilityEffectType').value = 'mp_regen_multiplier';
document.getElementById('utilityEffectValue').value = 2.0;
document.getElementById('utilityDurationHours').value = 1;
document.getElementById('utilityCooldownHours').value = 24;
document.getElementById('buffEffectType').value = 'defense_up';
document.getElementById('buffEffectPercent').value = 20;
document.getElementById('buffEffectDuration').value = 3;
}
// Toggle visibility of form sections based on skill type
@ -2782,8 +3068,15 @@
durationHours: parseFloat(document.getElementById('utilityDurationHours').value) || 1,
cooldownHours: parseFloat(document.getElementById('utilityCooldownHours').value) || 24
});
} else if (skillType === 'buff') {
// Combat buff skill - use buff config fields
statusEffect = JSON.stringify({
type: document.getElementById('buffEffectType').value,
percent: parseInt(document.getElementById('buffEffectPercent').value) || 20,
duration: parseInt(document.getElementById('buffEffectDuration').value) || 3
});
} else {
// Combat skill - use status effect fields
// Combat skill - use status effect fields (poison, burn, etc.)
const statusType = document.getElementById('skillStatusType').value;
if (statusType) {
statusEffect = JSON.stringify({
@ -2954,6 +3247,9 @@
// Populate skill dropdown for adding
populateClassSkillSelect();
// Initialize animation tester
initAnimationTester();
// Load class skills
await loadClassSkills(classData.id);
@ -2969,6 +3265,7 @@
currentClassSkills = [];
document.getElementById('classSkillsList').innerHTML = '<p style="color: #666; font-size: 12px;">Save class first, then edit to add skills.</p>';
populateClassSkillSelect();
initAnimationTester();
document.getElementById('classModal').classList.add('active');
});
@ -3110,6 +3407,15 @@
onchange="updateClassSkill(${cs.id}, 'choice_group', this.value || null)"
placeholder="-" title="Choice group (empty = auto-learn)">
</div>
<div class="skill-animation-section">
<label>Anim</label>
<div class="skill-animation-row">
<select class="skill-animation" id="anim-select-${cs.id}" onchange="updateClassSkill(${cs.id}, 'player_animation', this.value || null)" title="Player animation override">
${getPlayerAnimationOptionsHtml(cs.player_animation)}
</select>
<button type="button" class="anim-test-btn" onclick="testSkillAnimation(${cs.id})" title="Test animation"></button>
</div>
</div>
<button type="button" class="btn btn-danger btn-small" onclick="removeClassSkill(${cs.id})"></button>
</div>
`;

100
animations.js

@ -1,7 +1,71 @@
// HikeMap Monster Animation Definitions
// This file defines all available animations for monster icons
// HikeMap Animation Definitions
// This file defines all available animations for monster and player icons
// Edit the keyframes to customize how animations look
// Player-specific animations (defaults used when no skill override is set)
const PLAYER_ANIMATIONS = {
// Default attack animation - lunge forward towards enemies
attack: {
name: 'Attack',
description: 'Lunge forward towards enemy',
duration: 500,
loop: false,
easing: 'ease-out',
keyframes: `
0% { transform: translateX(0); }
20% { transform: translateX(-20px) scale(0.9); }
50% { transform: translateX(30px) scale(1.15); }
70% { transform: translateX(5px) scale(1.05); }
100% { transform: translateX(0) scale(1); }
`
},
// Default skill animation - quick pulse/glow effect
skill: {
name: 'Skill',
description: 'Quick pulse effect for skills',
duration: 400,
loop: false,
easing: 'ease-in-out',
keyframes: `
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
`
},
// Miss animation - attack motion then fall clockwise, hold, recover (mirrors monster miss)
miss: {
name: 'Miss',
description: 'Attack then fall over clockwise and recover',
duration: 2000,
loop: false,
easing: 'ease-out',
keyframes: `
0% { transform: translateX(0) rotate(0deg); }
10% { transform: translateX(-20px) scale(0.9) rotate(0deg); }
20% { transform: translateX(30px) scale(1.15) rotate(0deg); }
30% { transform: translateX(15px) rotate(90deg); }
70% { transform: translateX(15px) rotate(90deg); }
85% { transform: translateX(5px) rotate(30deg); }
100% { transform: translateX(0) rotate(0deg); }
`
},
// Death animation - fall over
death: {
name: 'Death',
description: 'Fall over permanently',
duration: 800,
loop: false,
easing: 'ease-out',
fillMode: 'forwards',
keyframes: `
0% { transform: rotate(0deg); opacity: 1; }
100% { transform: rotate(90deg); opacity: 0.5; }
`
}
};
const MONSTER_ANIMATIONS = {
// Default attack animation - rubber band snap towards player
attack: {
@ -169,8 +233,40 @@ function getAnimationList() {
}));
}
// Helper function to get player animation list for dropdowns
function getPlayerAnimationList() {
// Player can use both player-specific and monster animations
const playerAnims = Object.entries(PLAYER_ANIMATIONS).map(([id, anim]) => ({
id,
name: anim.name,
description: anim.description,
source: 'player'
}));
const monsterAnims = Object.entries(MONSTER_ANIMATIONS).map(([id, anim]) => ({
id,
name: anim.name,
description: anim.description,
source: 'monster'
}));
return [...playerAnims, ...monsterAnims];
}
// Get an animation by ID, checking both player and monster animations
function getAnimation(animationId) {
if (PLAYER_ANIMATIONS[animationId]) {
return PLAYER_ANIMATIONS[animationId];
}
if (MONSTER_ANIMATIONS[animationId]) {
return MONSTER_ANIMATIONS[animationId];
}
return null;
}
// Export for use in browser
if (typeof window !== 'undefined') {
window.MONSTER_ANIMATIONS = MONSTER_ANIMATIONS;
window.PLAYER_ANIMATIONS = PLAYER_ANIMATIONS;
window.getAnimationList = getAnimationList;
window.getPlayerAnimationList = getPlayerAnimationList;
window.getAnimation = getAnimation;
}

32
artwork_todo.md

@ -47,10 +47,10 @@ Track all emoji replacements and custom artwork needed. All icons should follow
| Emoji | Race | Art File | Status |
|-------|------|----------|--------|
| 👤 | Human | `races/human.png` | [ ] |
| 🧑 | Human | `races/human.png` | [ ] |
| 🧝 | Elf | `races/elf.png` | [ ] |
| ⛏️ | Dwarf | `races/dwarf.png` | [ ] |
| 🦶 | Halfling | `races/halfling.png` | [ ] |
| 🧔 | Dwarf | `races/dwarf.png` | [ ] |
| 🧒 | Halfling | `races/halfling.png` | [ ] |
---
@ -83,20 +83,20 @@ Need player character art to display in combat instead of class emoji.
---
## Skill Icons (Optional - currently use class icons or ⚔️)
## Skill Icons (Optional - currently use emojis in skill buttons)
Could add unique icons per skill for the combat UI skill buttons.
| Skill ID | Skill Name | Art File | Status |
|----------|------------|----------|--------|
| basic_attack | Attack / Kickems | `skills/basic_attack.png` | [ ] |
| double_attack | Double Attack / Brand New Hokas | `skills/double_attack.png` | [ ] |
| power_strike | Power Strike / Downhill Sprint | `skills/power_strike.png` | [ ] |
| heal | Heal / Gel Pack | `skills/heal.png` | [ ] |
| defend | Defend / Pace Yourself | `skills/defend.png` | [ ] |
| quick_step | Quick Step | `skills/quick_step.png` | [ ] |
| second_wind | Second Wind | `skills/second_wind.png` | [ ] |
| finish_line_sprint | Finish Line Sprint | `skills/finish_line_sprint.png` | [ ] |
| Emoji | Skill ID | Skill Name | Art File | Status |
|-------|----------|------------|----------|--------|
| 👊 | basic_attack | Attack | `skills/basic_attack.png` | [ ] |
| 👟 | brand_new_hokas | Brand New Hokas | `skills/brand_new_hokas.png` | [ ] |
| 🏃‍♂️ | downhill_sprint | Downhill Sprint | `skills/downhill_sprint.png` | [ ] |
| 🦵 | shin_kick | Shin Kick | `skills/shin_kick.png` | [ ] |
| ⚡ | quick_step | Quick Step | `skills/quick_step.png` | [ ] |
| 💨 | second_wind | Second Wind | `skills/second_wind.png` | [ ] |
| 🏁 | finish_line_sprint | Finish Line Sprint | `skills/finish_line_sprint.png` | [ ] |
| 🌀 | whirlwind | Whirlwind | `skills/whirlwind.png` | [ ] |
---
@ -110,9 +110,9 @@ Could add unique icons per skill for the combat UI skill buttons.
| Race Icons | 4 | Medium |
| UI Elements | 9 | Medium |
| Player Portraits | 4 (x2 sizes) | High |
| Skill Icons | 8+ | Low |
| Skill Icons | 8 | Low |
**Total unique artwork pieces needed: ~35-45**
**Total unique artwork pieces needed: ~43**
---

78
database.js

@ -291,6 +291,11 @@ class HikeMapDB {
this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN map_theme TEXT`);
} catch (e) { /* Column already exists */ }
// Migration: Add wander_range column for virtual movement limit
try {
this.db.exec(`ALTER TABLE rpg_stats ADD COLUMN wander_range INTEGER DEFAULT 200`);
} catch (e) { /* Column already exists */ }
// Migration: Add animation overrides to monster_types
try {
this.db.exec(`ALTER TABLE monster_types ADD COLUMN attack_animation TEXT DEFAULT 'attack'`);
@ -301,6 +306,9 @@ class HikeMapDB {
try {
this.db.exec(`ALTER TABLE monster_types ADD COLUMN idle_animation TEXT DEFAULT 'idle'`);
} catch (e) { /* Column already exists */ }
try {
this.db.exec(`ALTER TABLE monster_types ADD COLUMN miss_animation TEXT DEFAULT 'miss'`);
} catch (e) { /* Column already exists */ }
// Migration: Add animation override to monster_skills
try {
@ -369,6 +377,11 @@ class HikeMapDB {
this.db.exec(`ALTER TABLE monster_skills ADD COLUMN custom_icon TEXT`);
} catch (e) { /* Column already exists */ }
// Player animation for class skills - allows admin to assign animations per class skill override
try {
this.db.exec(`ALTER TABLE class_skills ADD COLUMN player_animation TEXT DEFAULT NULL`);
} catch (e) { /* Column already exists */ }
// OSM Tag settings - global prefix configuration
this.db.exec(`
CREATE TABLE IF NOT EXISTS osm_tag_settings (
@ -655,7 +668,7 @@ class HikeMapDB {
const stmt = this.db.prepare(`
SELECT character_name, race, class, level, xp, hp, max_hp, mp, max_mp, atk, def, accuracy, dodge,
unlocked_skills, active_skills,
home_base_lat, home_base_lng, last_home_set, is_dead, home_base_icon, data_version
home_base_lat, home_base_lng, last_home_set, is_dead, home_base_icon, data_version, wander_range
FROM rpg_stats WHERE user_id = ?
`);
return stmt.get(userId);
@ -736,8 +749,8 @@ class HikeMapDB {
const newVersion = currentVersion + 1;
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, active_skills, home_base_lat, home_base_lng, last_home_set, is_dead, data_version, 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, active_skills, home_base_lat, home_base_lng, last_home_set, is_dead, wander_range, data_version, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT(user_id) DO UPDATE SET
character_name = COALESCE(excluded.character_name, rpg_stats.character_name),
race = COALESCE(excluded.race, rpg_stats.race),
@ -758,6 +771,7 @@ class HikeMapDB {
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),
wander_range = COALESCE(excluded.wander_range, rpg_stats.wander_range),
data_version = excluded.data_version,
updated_at = datetime('now')
`);
@ -785,6 +799,7 @@ class HikeMapDB {
stats.homeBaseLng || null,
stats.lastHomeSet || null,
stats.isDead !== undefined ? (stats.isDead ? 1 : 0) : null,
stats.wanderRange || 200,
newVersion
);
@ -965,8 +980,8 @@ class HikeMapDB {
const stmt = this.db.prepare(`
INSERT INTO monster_types (id, name, icon, base_hp, base_atk, base_def, xp_reward,
level_scale_hp, level_scale_atk, level_scale_def, min_level, max_level, spawn_weight, dialogues, enabled,
base_mp, level_scale_mp, attack_animation, death_animation, idle_animation, spawn_location)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
base_mp, level_scale_mp, attack_animation, death_animation, idle_animation, miss_animation, spawn_location)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
// Support both camelCase (legacy) and snake_case (new admin UI) field names
const baseHp = monsterData.baseHp || monsterData.base_hp;
@ -992,6 +1007,7 @@ class HikeMapDB {
const attackAnim = monsterData.attack_animation || monsterData.attackAnimation || 'attack';
const deathAnim = monsterData.death_animation || monsterData.deathAnimation || 'death';
const idleAnim = monsterData.idle_animation || monsterData.idleAnimation || 'idle';
const missAnim = monsterData.miss_animation || monsterData.missAnimation || 'miss';
// Spawn location restriction
const spawnLocation = monsterData.spawn_location || monsterData.spawnLocation || 'anywhere';
@ -1016,6 +1032,7 @@ class HikeMapDB {
attackAnim,
deathAnim,
idleAnim,
missAnim,
spawnLocation
);
}
@ -1027,7 +1044,7 @@ class HikeMapDB {
xp_reward = ?, level_scale_hp = ?, level_scale_atk = ?, level_scale_def = ?,
min_level = ?, max_level = ?, spawn_weight = ?, dialogues = ?, enabled = ?,
base_mp = ?, level_scale_mp = ?, attack_animation = ?, death_animation = ?, idle_animation = ?,
spawn_location = ?
miss_animation = ?, spawn_location = ?
WHERE id = ?
`);
// Support both camelCase (legacy) and snake_case (new admin UI) field names
@ -1054,6 +1071,7 @@ class HikeMapDB {
const attackAnim = monsterData.attack_animation || monsterData.attackAnimation || 'attack';
const deathAnim = monsterData.death_animation || monsterData.deathAnimation || 'death';
const idleAnim = monsterData.idle_animation || monsterData.idleAnimation || 'idle';
const missAnim = monsterData.miss_animation || monsterData.missAnimation || 'miss';
// Spawn location restriction
const spawnLocation = monsterData.spawn_location || monsterData.spawnLocation || 'anywhere';
@ -1077,6 +1095,7 @@ class HikeMapDB {
attackAnim,
deathAnim,
idleAnim,
missAnim,
spawnLocation,
id
);
@ -1802,8 +1821,8 @@ class HikeMapDB {
createClassSkill(data) {
const stmt = this.db.prepare(`
INSERT INTO class_skills (class_id, skill_id, unlock_level, choice_group, custom_name, custom_description)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO class_skills (class_id, skill_id, unlock_level, choice_group, custom_name, custom_description, player_animation)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
return stmt.run(
data.class_id || data.classId,
@ -1811,23 +1830,44 @@ class HikeMapDB {
data.unlock_level || data.unlockLevel || 1,
data.choice_group || data.choiceGroup || null,
data.custom_name || data.customName || null,
data.custom_description || data.customDescription || null
data.custom_description || data.customDescription || null,
data.player_animation || data.playerAnimation || null
);
}
updateClassSkill(id, data) {
// Build dynamic update - only update fields that are provided
const updates = [];
const values = [];
if (data.unlock_level !== undefined || data.unlockLevel !== undefined) {
updates.push('unlock_level = ?');
values.push(data.unlock_level || data.unlockLevel || 1);
}
if (data.choice_group !== undefined || data.choiceGroup !== undefined) {
updates.push('choice_group = ?');
values.push(data.choice_group !== undefined ? data.choice_group : (data.choiceGroup || null));
}
if (data.custom_name !== undefined || data.customName !== undefined) {
updates.push('custom_name = ?');
values.push(data.custom_name !== undefined ? data.custom_name : (data.customName || null));
}
if (data.custom_description !== undefined || data.customDescription !== undefined) {
updates.push('custom_description = ?');
values.push(data.custom_description !== undefined ? data.custom_description : (data.customDescription || null));
}
if (data.player_animation !== undefined || data.playerAnimation !== undefined) {
updates.push('player_animation = ?');
values.push(data.player_animation !== undefined ? data.player_animation : (data.playerAnimation || null));
}
if (updates.length === 0) return { changes: 0 };
const stmt = this.db.prepare(`
UPDATE class_skills SET
unlock_level = ?, choice_group = ?, custom_name = ?, custom_description = ?
WHERE id = ?
UPDATE class_skills SET ${updates.join(', ')} WHERE id = ?
`);
return stmt.run(
data.unlock_level || data.unlockLevel || 1,
data.choice_group || data.choiceGroup || null,
data.custom_name || data.customName || null,
data.custom_description || data.customDescription || null,
id
);
values.push(id);
return stmt.run(...values);
}
deleteClassSkill(id) {

272
geocaches.json

@ -126,9 +126,18 @@
"tags": [
"grocery"
],
"messages": [],
"messages": [
{
"author": "Melancholytron",
"text": "Beware the George!",
"timestamp": 1767887532080
}
],
"createdAt": 1736309000000,
"alerted": false
"alerted": false,
"artwork": null,
"spawnRadius": 200,
"artworkNum": 1
},
{
"id": "gc_grocery_heb",
@ -143,7 +152,10 @@
],
"messages": [],
"createdAt": 1736309000000,
"alerted": false
"alerted": false,
"artwork": null,
"spawnRadius": 150,
"artworkNum": 1
},
{
"id": "gc_osm_588035848",
@ -779,7 +791,7 @@
"id": "gc_osm_7744500838",
"lat": 30.5026623,
"lng": -97.8210497,
"title": "Teriyaki Tom\u2019s",
"title": "Teriyaki Toms",
"icon": "silverware-fork-knife",
"color": "#e91e63",
"visibilityDistance": 50,
@ -2394,5 +2406,257 @@
"messages": [],
"createdAt": 1736400000000,
"alerted": false
},
{
"id": "gc_osm_29169603",
"lat": 30.5236988,
"lng": -97.8321183,
"title": "Chili's",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_29169617",
"lat": 30.5208648,
"lng": -97.8307028,
"title": "Taqueria La Tapatia",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_29195102",
"lat": 30.5115987,
"lng": -97.8245959,
"title": "El Patron Tacos & More",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_103477381",
"lat": 30.5298695,
"lng": -97.8160544,
"title": "Buffalo Wild Wings",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_103480088",
"lat": 30.5229788,
"lng": -97.8212726,
"title": "IHOP",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_131466568",
"lat": 30.5267884,
"lng": -97.8134028,
"title": "Jack Allen's Kitchen",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_344620444",
"lat": 30.5255136,
"lng": -97.8173785,
"title": "Tumble 22",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_344620447",
"lat": 30.5251465,
"lng": -97.8188497,
"title": "BJ's",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_344620450",
"lat": 30.5260406,
"lng": -97.8177921,
"title": "Lupe Tortilla Mexican Restaurant",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_744508526",
"lat": 30.5322274,
"lng": -97.8183006,
"title": "Chuy's",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_865199012",
"lat": 30.5300743,
"lng": -97.8184667,
"title": "Jason's Deli",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_865199014",
"lat": 30.5330055,
"lng": -97.8181929,
"title": "Red Robin",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_1452010758",
"lat": 30.5153358,
"lng": -97.8491592,
"title": "Masala Grill",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_1452010759",
"lat": 30.5156305,
"lng": -97.8489589,
"title": "Tacos Las Mamis",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_1453752902",
"lat": 30.5358753,
"lng": -97.815246,
"title": "Five Four Restaurant & Drafthouse - Cedar Park",
"icon": "silverware-fork-knife",
"color": "#4CAF50",
"tags": [
"restaurant"
],
"messages": [],
"createdAt": 1768018168162,
"autoDiscovered": true
},
{
"id": "gc_osm_555259415",
"lat": 30.5358552,
"lng": -97.8177075,
"title": "Whole Foods Market",
"icon": "cart",
"color": "#4CAF50",
"tags": [
"grocery"
],
"messages": [],
"createdAt": 1768077245617,
"autoDiscovered": true
},
{
"id": "gc_osm_605094231",
"lat": 30.5430089,
"lng": -97.8652297,
"title": "Randalls",
"icon": "cart",
"color": "#4CAF50",
"tags": [
"grocery"
],
"messages": [],
"createdAt": 1768077245617,
"autoDiscovered": true
},
{
"id": "gc_osm_964446665",
"lat": 30.5277795,
"lng": -97.8157961,
"title": "Natural Grocers",
"icon": "cart",
"color": "#4CAF50",
"tags": [
"grocery"
],
"messages": [],
"createdAt": 1768077245617,
"autoDiscovered": true
}
]

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

BIN
mapgameimgs/monsters/karen_base100.png

After

Width: 100  |  Height: 100  |  Size: 11 KiB

BIN
mapgameimgs/monsters/karen_base50.png

After

Width: 50  |  Height: 50  |  Size: 4.8 KiB

BIN
mapgameimgs/skills/full_restore.png

Before

Width: 50  |  Height: 50  |  Size: 3.1 KiB

After

Width: 50  |  Height: 50  |  Size: 3.1 KiB

BIN
mapgamemusic/homebase.mp3

4
server.js

@ -853,6 +853,7 @@ app.get('/api/user/rpg-stats', authenticateToken, (req, res) => {
isDead: !!stats.is_dead,
homeBaseIcon: stats.home_base_icon || '00',
revealRadius: stats.reveal_radius || 800,
wanderRange: stats.wander_range || 200,
dataVersion: stats.data_version || 1
});
} else {
@ -1378,6 +1379,7 @@ app.get('/api/monster-types', (req, res) => {
attackAnimation: t.attack_animation || 'attack',
deathAnimation: t.death_animation || 'death',
idleAnimation: t.idle_animation || 'idle',
missAnimation: t.miss_animation || 'miss',
// Spawn location restriction
spawnLocation: t.spawn_location || 'anywhere'
}));
@ -2742,9 +2744,11 @@ wss.on('connection', (ws) => {
} else if (data.type === 'geocacheUpdate') {
// Handle geocache creation/update
if (data.geocache) {
console.log(`[GEOCACHE] Received update for cache ${data.geocache.id}, spawnRadius=${data.geocache.spawnRadius}, tags=${JSON.stringify(data.geocache.tags)}`);
const existingIndex = geocaches.findIndex(g => g.id === data.geocache.id);
if (existingIndex >= 0) {
// Update existing geocache
console.log(`[GEOCACHE] Updating existing cache at index ${existingIndex}`);
geocaches[existingIndex] = data.geocache;
} else {
// Add new geocache

7
service-worker.js

@ -1,6 +1,6 @@
// HikeMap Service Worker
// Increment version to force cache refresh
const CACHE_NAME = 'hikemap-v1.2.0';
const CACHE_NAME = 'hikemap-v1.5.0';
const urlsToCache = [
'/',
'/index.html',
@ -9,9 +9,8 @@ const urlsToCache = [
'/animations.js',
'/icon-192x192.png',
'/icon-512x512.png',
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css',
'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js',
'https://unpkg.com/leaflet-rotate@0.2.8/dist/leaflet-rotate-src.js',
'https://unpkg.com/maplibre-gl@4.1.0/dist/maplibre-gl.css',
'https://unpkg.com/maplibre-gl@4.1.0/dist/maplibre-gl.js',
'https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css'
];

Loading…
Cancel
Save