From 2a07c89e1c65e48c5b4902d5dadf64e1ba6ea7ee Mon Sep 17 00:00:00 2001 From: HikeMap User Date: Mon, 19 Jan 2026 11:14:02 -0600 Subject: [PATCH] Simplify fog of war, fix combat timing, update docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 87 +- README.md | 18 +- admin.html | 330 +++- animations.js | 100 +- artwork_todo.md | 32 +- database.js | 78 +- geocaches.json | 272 ++- index.html | 2234 +++++++++++++++++++----- mapgameimgs/monsters/karen_base100.png | Bin 0 -> 11206 bytes mapgameimgs/monsters/karen_base50.png | Bin 0 -> 4938 bytes mapgameimgs/skills/full_restore.png | Bin 3184 -> 3183 bytes mapgamemusic/homebase.mp3 | Bin 258026 -> 1457360 bytes server.js | 4 + service-worker.js | 7 +- 14 files changed, 2632 insertions(+), 530 deletions(-) create mode 100755 mapgameimgs/monsters/karen_base100.png create mode 100755 mapgameimgs/monsters/karen_base50.png diff --git a/CLAUDE.md b/CLAUDE.md index 6ea8c5b..0d980ac 100644 --- a/CLAUDE.md +++ b/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? | diff --git a/README.md b/README.md index a3242da..15ce56d 100644 --- a/README.md +++ b/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) diff --git a/admin.html b/admin.html index c7ff222..a617b3a 100644 --- a/admin.html +++ b/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 @@ +
+ + +

Animation Preview

@@ -1506,6 +1599,33 @@ + + +
@@ -1672,6 +1793,23 @@ Assign skills to this class. Set unlock level and choice group for skill selection at level-up.
Skills with the same choice group at the same level = player picks one.

+ + +
+

🎬 Animation Tester

+
+
+ +
+
+ + +
+
+
+
+ ${getPlayerAnimationOptionsHtml(cs.player_animation)} + + +
+
`; diff --git a/animations.js b/animations.js index b747c8a..43447a6 100644 --- a/animations.js +++ b/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; } diff --git a/artwork_todo.md b/artwork_todo.md index f2c1cc9..0a43076 100644 --- a/artwork_todo.md +++ b/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** --- diff --git a/database.js b/database.js index c893e82..1b9beab 100644 --- a/database.js +++ b/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) { diff --git a/geocaches.json b/geocaches.json index d0e6335..b634eb0 100644 --- a/geocaches.json +++ b/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 Tom’s", "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 } ] \ No newline at end of file diff --git a/index.html b/index.html index c60f21e..03128b1 100644 --- a/index.html +++ b/index.html @@ -1954,6 +1954,7 @@ width: 32px; height: 32px; object-fit: contain; + image-rendering: pixelated; } .char-sheet-monster .monster-info { flex: 1; @@ -2039,6 +2040,130 @@ font-weight: bold; } + /* Monster Spawn Modal */ + .monster-spawn-modal { + background: linear-gradient(135deg, #1a1a2e 0%, #0f0f23 100%); + border-radius: 16px; + padding: 24px; + max-width: 400px; + width: 90%; + border: 2px solid #ff6b35; + box-shadow: 0 0 30px rgba(255, 107, 53, 0.3); + } + .monster-spawn-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + } + .monster-spawn-header h2 { + color: #ff6b35; + margin: 0; + font-size: 24px; + } + .monster-spawn-close { + background: none; + border: none; + color: #aaa; + font-size: 28px; + cursor: pointer; + padding: 0; + line-height: 1; + } + .monster-spawn-close:hover { + color: #fff; + } + .monster-spawn-search { + position: relative; + margin-bottom: 16px; + } + .monster-spawn-search input { + width: 100%; + padding: 12px 16px; + font-size: 16px; + border: 2px solid #333; + border-radius: 8px; + background: rgba(0, 0, 0, 0.4); + color: #fff; + box-sizing: border-box; + } + .monster-spawn-search input:focus { + outline: none; + border-color: #ff6b35; + } + .monster-search-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: #1a1a2e; + border: 2px solid #333; + border-top: none; + border-radius: 0 0 8px 8px; + max-height: 250px; + overflow-y: auto; + z-index: 10; + display: none; + } + .monster-search-results.active { + display: block; + } + .monster-search-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + cursor: pointer; + transition: background 0.2s; + border-bottom: 1px solid #333; + } + .monster-search-item:last-child { + border-bottom: none; + } + .monster-search-item:hover { + background: rgba(255, 107, 53, 0.2); + } + .monster-search-item img { + width: 40px; + height: 40px; + object-fit: contain; + image-rendering: pixelated; + } + .monster-search-item .monster-info { + flex: 1; + } + .monster-search-item .monster-name { + font-weight: bold; + color: #fff; + } + .monster-search-item .monster-level-range { + font-size: 12px; + color: #888; + } + .monster-spawn-options { + margin-top: 16px; + } + .monster-spawn-option { + display: flex; + align-items: center; + gap: 12px; + color: #ccc; + } + .monster-spawn-option input[type="number"] { + width: 70px; + padding: 8px 12px; + font-size: 14px; + border: 2px solid #333; + border-radius: 6px; + background: rgba(0, 0, 0, 0.4); + color: #fff; + text-align: center; + } + .monster-spawn-option input[type="number"]:focus { + outline: none; + border-color: #ff6b35; + } + /* User Profile Display */ .user-profile { display: flex; @@ -2266,6 +2391,10 @@ border: none !important; pointer-events: auto !important; touch-action: none; + /* Prevent blurry subpixel rendering */ + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; } .monster-marker { position: relative; @@ -2275,8 +2404,8 @@ display: flex; align-items: center; justify-content: center; - width: 70px; - height: 70px; + width: 50px; + height: 50px; /* Semi-transparent background for tap area */ background: radial-gradient(circle, rgba(255,100,100,0.25) 0%, rgba(255,100,100,0) 70%); border-radius: 50%; @@ -2298,18 +2427,33 @@ } @keyframes monster-combat-zoom { 0% { transform: scale(1); } - 100% { transform: scale(5); } + 100% { transform: scale(1); } /* Container stays same size */ + } + /* Animate the image directly for crisp scaling */ + .monster-marker.combat-zoom-in .monster-icon { + animation: monster-icon-combat-zoom 1.2s ease-in-out forwards; + filter: none; /* Remove filter during zoom to prevent rasterization */ + } + @keyframes monster-icon-combat-zoom { + 0% { transform: scale(1) translateY(0) translateZ(0); } + 100% { transform: scale(5) translateY(0) translateZ(0); } /* 50px * 5 = 250px */ } .monster-icon { width: 50px; height: 50px; object-fit: contain; - filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.6)); + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; + /* Prevent subpixel antialiasing blur */ + transform: translateZ(0); + backface-visibility: hidden; + -webkit-backface-visibility: hidden; animation: monster-bob 2s ease-in-out infinite; } @keyframes monster-bob { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-6px); } + 0%, 100% { transform: translateY(0) translateZ(0); } + 50% { transform: translateY(-6px) translateZ(0); } } .monster-dialogue-bubble { position: absolute; @@ -2399,6 +2543,53 @@ color: #aaa; } + /* Wander Distance Indicator */ + .wander-distance-indicator { + position: fixed; + top: 90px; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.85); + color: #ff9800; + padding: 6px 14px; + border-radius: 16px; + font-size: 12px; + font-weight: bold; + z-index: 999; + border: 2px solid #ff9800; + box-shadow: 0 2px 10px rgba(255, 152, 0, 0.3); + } + .wander-distance-indicator.warning { + color: #ff5722; + border-color: #ff5722; + animation: pulse-warning 0.5s ease-in-out infinite; + } + @keyframes pulse-warning { + 0%, 100% { box-shadow: 0 2px 10px rgba(255, 87, 34, 0.3); } + 50% { box-shadow: 0 2px 20px rgba(255, 87, 34, 0.6); } + } + + /* Wander Danger Overlay - flashing red/black warning when beyond wander range */ + .wander-danger-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(200, 0, 0, 0.5); + pointer-events: none; + z-index: 998; + opacity: 0; + transition: opacity 0.3s ease; + } + .wander-danger-overlay.active { + animation: danger-siren 0.5s ease-in-out infinite; + } + @keyframes danger-siren { + 0%, 100% { background: rgba(200, 0, 0, 0.5); } + 50% { background: rgba(0, 0, 0, 0.5); } + } + /* Combat Overlay Styles */ .combat-overlay { position: fixed; @@ -2498,6 +2689,45 @@ color: #aaa; margin-top: 5px; } + /* Mini enemy roster - small icons showing all enemies */ + .mini-enemy-roster { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + padding: 8px; + margin-bottom: 8px; + background: rgba(0, 0, 0, 0.3); + border-radius: 8px; + min-height: 35px; + } + .mini-enemy-icon { + width: 25px; + height: 25px; + object-fit: contain; + image-rendering: pixelated; + border: 2px solid #444; + border-radius: 4px; + cursor: pointer; + transition: border-color 0.2s, opacity 0.2s, transform 0.1s; + } + .mini-enemy-icon:hover { + border-color: #888; + transform: scale(1.1); + } + .mini-enemy-icon.selected { + border-color: #e94560; + box-shadow: 0 0 6px rgba(233, 69, 96, 0.5); + } + .mini-enemy-icon.dead { + opacity: 0.4; + filter: grayscale(100%); + } + .mini-enemy-icon.attacking { + border-color: #ff6b35; + box-shadow: 0 0 6px rgba(255, 107, 53, 0.6); + } + .combat-log { background: rgba(0, 0, 0, 0.5); border-radius: 8px; @@ -2787,6 +3017,7 @@ width: var(--combat-icon-size); height: var(--combat-icon-size); object-fit: contain; + image-rendering: pixelated; } .monster-entry .sprite-container { position: relative; @@ -3023,55 +3254,6 @@ grid-column: 3; grid-row: 2; } - .wasd-mode-indicator { - position: absolute; - top: -25px; - left: 50%; - transform: translateX(-50%); - background: rgba(0, 0, 0, 0.7); - color: #4fc3f7; - padding: 3px 8px; - border-radius: 4px; - font-size: 10px; - white-space: nowrap; - } - - /* Compass/GPS Button */ - .compass-btn { - position: fixed; - bottom: 135px; - left: 62px; - z-index: 2100; - width: 50px; - height: 50px; - border-radius: 50%; - background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%); - border: 2px solid #4a6785; - color: #fff; - font-size: 24px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4); - transition: all 0.2s; - opacity: 0.85; - } - .compass-btn:active { - transform: scale(0.95); - } - .compass-btn.active { - background: linear-gradient(135deg, #27ae60 0%, #1e8449 100%); - border-color: #2ecc71; - animation: pulse-glow 2s infinite; - } - .compass-btn.hidden { - display: none; - } - @keyframes pulse-glow { - 0%, 100% { box-shadow: 0 3px 10px rgba(0, 0, 0, 0.4); } - 50% { box-shadow: 0 3px 15px rgba(46, 204, 113, 0.6); } - } /* Home Base Marker */ .home-base-marker { @@ -3668,6 +3850,8 @@
+ +
N
@@ -3695,18 +3879,19 @@
+ + + - - - - - +
+
Combat begins!
@@ -3891,23 +4078,26 @@ -