5 changed files with 1354 additions and 66 deletions
@ -0,0 +1,589 @@
@@ -0,0 +1,589 @@
|
||||
/** |
||||
* Centralized event handling system for Birthday Spire |
||||
* Manages all user interactions, keyboard shortcuts, and UI events |
||||
*/ |
||||
|
||||
export class EventHandler { |
||||
constructor(root) { |
||||
this.root = root; |
||||
this.listeners = new Map(); // Track active listeners for cleanup
|
||||
this.keyHandlers = new Map(); // Track keyboard shortcuts
|
||||
this.globalHandlers = new Set(); // Track global event handlers
|
||||
this.currentScreen = null; |
||||
|
||||
this.setupGlobalEvents(); |
||||
} |
||||
|
||||
/** |
||||
* Setup global event handlers that persist across screens |
||||
*/ |
||||
setupGlobalEvents() { |
||||
// Global keyboard handler
|
||||
this.globalKeyHandler = (e) => { |
||||
const handler = this.keyHandlers.get(e.key.toLowerCase()); |
||||
if (handler) { |
||||
e.preventDefault(); |
||||
handler(e); |
||||
} |
||||
}; |
||||
|
||||
document.addEventListener('keydown', this.globalKeyHandler); |
||||
this.globalHandlers.add('keydown'); |
||||
|
||||
// Global escape handler for modals
|
||||
this.globalEscapeHandler = (e) => { |
||||
if (e.key === 'Escape') { |
||||
this.closeTopModal(); |
||||
} |
||||
}; |
||||
|
||||
document.addEventListener('keydown', this.globalEscapeHandler); |
||||
this.globalHandlers.add('escape'); |
||||
} |
||||
|
||||
/** |
||||
* Switch to a new screen and cleanup old events |
||||
*/ |
||||
switchScreen(screenName) { |
||||
this.cleanup(); |
||||
this.currentScreen = screenName; |
||||
this.keyHandlers.clear(); |
||||
} |
||||
|
||||
/** |
||||
* Add event listener with automatic tracking for cleanup |
||||
*/ |
||||
on(element, event, handler, options = {}) { |
||||
if (!element) return; |
||||
|
||||
const wrappedHandler = (e) => { |
||||
try { |
||||
handler(e); |
||||
} catch (error) { |
||||
console.error(`Event handler error (${event}):`, error); |
||||
} |
||||
}; |
||||
|
||||
element.addEventListener(event, wrappedHandler, options); |
||||
|
||||
// Track for cleanup
|
||||
if (!this.listeners.has(element)) { |
||||
this.listeners.set(element, []); |
||||
} |
||||
this.listeners.get(element).push({ event, handler: wrappedHandler, options }); |
||||
} |
||||
|
||||
/** |
||||
* Add keyboard shortcut |
||||
*/ |
||||
addKeyHandler(key, handler, description = '') { |
||||
this.keyHandlers.set(key.toLowerCase(), handler); |
||||
} |
||||
|
||||
/** |
||||
* Remove keyboard shortcut |
||||
*/ |
||||
removeKeyHandler(key) { |
||||
this.keyHandlers.delete(key.toLowerCase()); |
||||
} |
||||
|
||||
/** |
||||
* Battle screen event handlers |
||||
*/ |
||||
setupBattleEvents() { |
||||
this.switchScreen('battle'); |
||||
|
||||
// Card play events
|
||||
this.root.app.querySelectorAll("[data-play]").forEach(btn => { |
||||
this.on(btn, "mouseenter", () => { |
||||
if (btn.classList.contains('playable')) { |
||||
this.playSound('swipe.mp3'); |
||||
this.root.selectedCardIndex = null; |
||||
this.updateCardSelection(); |
||||
} |
||||
}); |
||||
|
||||
this.on(btn, "click", () => { |
||||
const index = parseInt(btn.dataset.play, 10); |
||||
const card = this.root.player.hand[index]; |
||||
if (this.root.player.energy >= card.cost) { |
||||
this.playSound('played-card.mp3'); |
||||
this.root.play(index); |
||||
this.root.selectedCardIndex = null; |
||||
this.updateCardSelection(); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
// End turn button
|
||||
const endTurnBtn = this.root.app.querySelector("[data-action='end']"); |
||||
if (endTurnBtn) { |
||||
this.on(endTurnBtn, "click", () => { |
||||
try { |
||||
this.root.end(); |
||||
} catch (error) { |
||||
console.error("Error ending turn:", error); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
// Keyboard shortcuts for battle
|
||||
this.addKeyHandler('e', () => { |
||||
try { |
||||
this.root.end(); |
||||
} catch (error) { |
||||
console.error("Error ending turn via keyboard:", error); |
||||
} |
||||
}, 'End Turn'); |
||||
|
||||
// Number keys for card selection and play
|
||||
for (let i = 1; i <= 9; i++) { |
||||
this.addKeyHandler(i.toString(), (e) => { |
||||
const cardIndex = i - 1; |
||||
const hand = this.root.player.hand; |
||||
|
||||
if (cardIndex >= hand.length) return; |
||||
|
||||
const card = hand[cardIndex]; |
||||
|
||||
if (this.root.selectedCardIndex === cardIndex) { |
||||
// Second press - play the card
|
||||
if (this.root.player.energy >= card.cost) { |
||||
this.root.play(cardIndex); |
||||
this.root.selectedCardIndex = null; |
||||
this.updateCardSelection(); |
||||
} |
||||
} else { |
||||
// First press - select the card
|
||||
this.root.selectedCardIndex = cardIndex; |
||||
this.updateCardSelection(); |
||||
this.playSound('swipe.mp3'); |
||||
} |
||||
}, `Select/Play Card ${i}`); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Map screen event handlers |
||||
*/ |
||||
setupMapEvents() { |
||||
this.switchScreen('map'); |
||||
|
||||
// Node navigation
|
||||
this.root.app.querySelectorAll("[data-node]").forEach(el => { |
||||
if (!el.dataset.node) return; |
||||
|
||||
this.on(el, "click", () => this.root.go(el.dataset.node)); |
||||
}); |
||||
|
||||
// Tooltip handling for all nodes (including non-clickable ones)
|
||||
this.root.app.querySelectorAll(".spire-node").forEach(el => { |
||||
this.on(el, "mouseenter", (e) => this.showTooltip(e)); |
||||
this.on(el, "mouseleave", () => this.hideTooltip()); |
||||
}); |
||||
|
||||
// Messages button
|
||||
const messagesBtn = this.root.app.querySelector("[data-action='show-messages']"); |
||||
if (messagesBtn) { |
||||
this.on(messagesBtn, "click", () => this.showMessagesModal()); |
||||
} |
||||
|
||||
// Reset button
|
||||
const resetBtn = this.root.app.querySelector("[data-reset]"); |
||||
if (resetBtn) { |
||||
this.on(resetBtn, "click", () => { |
||||
this.root.clearSave(); |
||||
this.root.reset(); |
||||
}); |
||||
} |
||||
|
||||
// Keyboard shortcut for messages
|
||||
this.addKeyHandler('m', () => this.showMessagesModal(), 'Show Messages'); |
||||
} |
||||
|
||||
/** |
||||
* Reward screen event handlers |
||||
*/ |
||||
setupRewardEvents(choices) { |
||||
this.switchScreen('reward'); |
||||
|
||||
this.root.app.querySelectorAll("[data-pick]").forEach(btn => { |
||||
this.on(btn, "click", () => { |
||||
const idx = parseInt(btn.dataset.pick, 10); |
||||
this.root.takeReward(idx); |
||||
}); |
||||
}); |
||||
|
||||
const skipBtn = this.root.app.querySelector("[data-skip]"); |
||||
if (skipBtn) { |
||||
this.on(skipBtn, "click", () => this.root.skipReward()); |
||||
} |
||||
|
||||
// Keyboard shortcuts for reward selection
|
||||
for (let i = 1; i <= choices.length; i++) { |
||||
this.addKeyHandler(i.toString(), () => { |
||||
this.root.takeReward(i - 1); |
||||
}, `Select Reward ${i}`); |
||||
} |
||||
|
||||
this.addKeyHandler('s', () => this.root.skipReward(), 'Skip Reward'); |
||||
} |
||||
|
||||
/** |
||||
* Rest screen event handlers |
||||
*/ |
||||
setupRestEvents() { |
||||
this.switchScreen('rest'); |
||||
|
||||
const healBtn = this.root.app.querySelector("[data-act='heal']"); |
||||
const upgradeBtn = this.root.app.querySelector("[data-act='upgrade']"); |
||||
|
||||
if (healBtn) { |
||||
this.on(healBtn, "click", () => { |
||||
const heal = Math.floor(this.root.player.maxHp * 0.2); |
||||
this.root.player.hp = Math.min(this.root.player.maxHp, this.root.player.hp + heal); |
||||
this.root.log(`Rested: +${heal} HP`); |
||||
this.root.afterNode(); |
||||
}); |
||||
} |
||||
|
||||
if (upgradeBtn) { |
||||
this.on(upgradeBtn, "click", () => { |
||||
// Import and call renderUpgrade
|
||||
import("../ui/render.js").then(({ renderUpgrade }) => { |
||||
renderUpgrade(this.root); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
// Keyboard shortcuts
|
||||
this.addKeyHandler('h', () => healBtn?.click(), 'Heal'); |
||||
this.addKeyHandler('u', () => upgradeBtn?.click(), 'Upgrade'); |
||||
} |
||||
|
||||
/** |
||||
* Shop screen event handlers |
||||
*/ |
||||
setupShopEvents(shopCards = [], shopRelic = null) { |
||||
this.switchScreen('shop'); |
||||
|
||||
// Card purchase events
|
||||
this.root.app.querySelectorAll("[data-buy-card]").forEach(btn => { |
||||
this.on(btn, "click", () => { |
||||
const idx = parseInt(btn.dataset.buyCard, 10); |
||||
const card = shopCards[idx]; |
||||
if (this.root.player.gold >= 50) { |
||||
this.root.player.gold -= 50; |
||||
this.root.player.deck.push(card.id); |
||||
this.root.log(`Bought ${card.name} for 50 gold.`); |
||||
btn.disabled = true; |
||||
btn.textContent = "SOLD"; |
||||
|
||||
this.updateGoldDisplay(); |
||||
this.updateShopAffordability(); |
||||
} else { |
||||
this.root.log("Not enough gold!"); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
// Relic purchase
|
||||
if (shopRelic) { |
||||
const relicBtn = this.root.app.querySelector("[data-buy-relic]"); |
||||
if (relicBtn) { |
||||
this.on(relicBtn, "click", async () => { |
||||
if (this.root.player.gold >= 100) { |
||||
this.root.player.gold -= 100; |
||||
this.root.log(`Bought ${shopRelic.name} for 100 gold.`); |
||||
|
||||
const { attachRelics } = await import("../engine/battle.js"); |
||||
const currentRelicIds = this.root.relicStates.map(r => r.id); |
||||
const newRelicIds = [...currentRelicIds, shopRelic.id]; |
||||
attachRelics(this.root, newRelicIds); |
||||
|
||||
relicBtn.disabled = true; |
||||
relicBtn.textContent = "SOLD"; |
||||
|
||||
this.updateGoldDisplay(); |
||||
this.updateShopAffordability(); |
||||
} else { |
||||
this.root.log("Not enough gold!"); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
// Leave shop
|
||||
const leaveBtn = this.root.app.querySelector("[data-leave]"); |
||||
if (leaveBtn) { |
||||
this.on(leaveBtn, "click", () => this.root.afterNode()); |
||||
} |
||||
|
||||
// Keyboard shortcuts
|
||||
this.addKeyHandler('escape', () => this.root.afterNode(), 'Leave Shop'); |
||||
this.addKeyHandler('l', () => this.root.afterNode(), 'Leave Shop'); |
||||
} |
||||
|
||||
/** |
||||
* Event screen event handlers |
||||
*/ |
||||
setupEventEvents(event) { |
||||
this.switchScreen('event'); |
||||
|
||||
this.root.app.querySelectorAll("[data-choice]").forEach(btn => { |
||||
this.on(btn, "click", () => { |
||||
const idx = parseInt(btn.dataset.choice, 10); |
||||
event.choices[idx].effect(); |
||||
this.root.afterNode(); |
||||
}); |
||||
}); |
||||
|
||||
// Keyboard shortcuts for event choices
|
||||
for (let i = 1; i <= event.choices.length; i++) { |
||||
this.addKeyHandler(i.toString(), () => { |
||||
event.choices[i - 1].effect(); |
||||
this.root.afterNode(); |
||||
}, `Event Choice ${i}`); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Relic selection event handlers |
||||
*/ |
||||
setupRelicSelectionEvents(relicChoices) { |
||||
this.switchScreen('relic-selection'); |
||||
|
||||
this.root.app.querySelectorAll("[data-relic]").forEach(btn => { |
||||
this.on(btn, "click", () => { |
||||
const relicId = btn.dataset.relic; |
||||
this.root.selectStartingRelic(relicId); |
||||
}); |
||||
}); |
||||
|
||||
// Messages button
|
||||
const messagesBtn = this.root.app.querySelector("[data-action='show-messages']"); |
||||
if (messagesBtn) { |
||||
this.on(messagesBtn, "click", () => this.showMessagesModal()); |
||||
} |
||||
|
||||
// Keyboard shortcuts
|
||||
for (let i = 1; i <= relicChoices.length; i++) { |
||||
this.addKeyHandler(i.toString(), () => { |
||||
const relicBtn = this.root.app.querySelector(`[data-relic="${relicChoices[i-1]}"]`); |
||||
relicBtn?.click(); |
||||
}, `Select Relic ${i}`); |
||||
} |
||||
|
||||
this.addKeyHandler('m', () => this.showMessagesModal(), 'Show Messages'); |
||||
} |
||||
|
||||
/** |
||||
* Win/Lose screen event handlers |
||||
*/ |
||||
setupEndGameEvents() { |
||||
this.switchScreen('endgame'); |
||||
|
||||
const replayBtn = this.root.app.querySelector("[data-replay]"); |
||||
const restartAct2Btn = this.root.app.querySelector("[data-restart-act2]"); |
||||
const menuBtn = this.root.app.querySelector("[data-menu]"); |
||||
|
||||
if (replayBtn) { |
||||
this.on(replayBtn, "click", () => this.root.reset()); |
||||
} |
||||
|
||||
if (restartAct2Btn) { |
||||
this.on(restartAct2Btn, "click", async () => { |
||||
if (this.root.loadAct2Checkpoint()) { |
||||
const { renderMap } = await import("../ui/render.js"); |
||||
await renderMap(this.root); |
||||
} else { |
||||
this.root.reset(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
if (menuBtn) { |
||||
this.on(menuBtn, "click", () => this.root.reset()); |
||||
} |
||||
|
||||
// Keyboard shortcuts
|
||||
this.addKeyHandler('r', () => replayBtn?.click(), 'Replay'); |
||||
this.addKeyHandler('2', () => restartAct2Btn?.click(), 'Restart Act 2'); |
||||
this.addKeyHandler('m', () => menuBtn?.click(), 'Main Menu'); |
||||
} |
||||
|
||||
/** |
||||
* Utility methods |
||||
*/ |
||||
updateCardSelection() { |
||||
// Remove selection from all cards
|
||||
this.root.app.querySelectorAll('.battle-card').forEach(card => { |
||||
card.classList.remove('card-selected'); |
||||
}); |
||||
|
||||
// Add selection to currently selected card
|
||||
if (this.root.selectedCardIndex !== null) { |
||||
const selectedCard = this.root.app.querySelector(`[data-play="${this.root.selectedCardIndex}"]`); |
||||
if (selectedCard) { |
||||
selectedCard.classList.add('card-selected'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
updateGoldDisplay() { |
||||
const goldDisplay = this.root.app.querySelector('.gold-amount'); |
||||
if (goldDisplay) { |
||||
goldDisplay.textContent = this.root.player.gold; |
||||
} |
||||
} |
||||
|
||||
updateShopAffordability() { |
||||
// Implementation would go here - update visual affordability indicators
|
||||
// This would need to be implemented based on the current shop logic
|
||||
} |
||||
|
||||
playSound(soundFile) { |
||||
try { |
||||
const audio = new Audio(`assets/sounds/${soundFile}`); |
||||
audio.volume = 0.3; |
||||
audio.play().catch(e => console.log(e)); |
||||
} catch (e) { |
||||
// Silently fail if audio not available
|
||||
} |
||||
} |
||||
|
||||
showTooltip(event) { |
||||
const tooltip = document.getElementById('custom-tooltip'); |
||||
if (!tooltip) return; |
||||
|
||||
const node = event.target.closest('.spire-node'); |
||||
if (!node) return; |
||||
|
||||
const content = node.dataset.tooltip; |
||||
const avatarPath = node.dataset.avatar; |
||||
|
||||
let tooltipHTML = ''; |
||||
if (avatarPath) { |
||||
tooltipHTML = ` |
||||
<div class="tooltip-with-avatar"> |
||||
<div class="tooltip-avatar"> |
||||
<img src="${avatarPath}" alt="Enemy Avatar" class="tooltip-avatar-img"
|
||||
onerror="this.style.display='none';"> |
||||
</div> |
||||
<div class="tooltip-content">${content}</div> |
||||
</div> |
||||
`;
|
||||
} else { |
||||
tooltipHTML = content; |
||||
} |
||||
|
||||
tooltip.innerHTML = tooltipHTML; |
||||
tooltip.style.display = 'block'; |
||||
|
||||
// Position tooltip
|
||||
const rect = node.getBoundingClientRect(); |
||||
tooltip.style.left = (rect.right + 15) + 'px'; |
||||
tooltip.style.top = (rect.top + rect.height / 2 - tooltip.offsetHeight / 2) + 'px'; |
||||
|
||||
// Keep tooltip in viewport
|
||||
const tooltipRect = tooltip.getBoundingClientRect(); |
||||
if (tooltipRect.right > window.innerWidth) { |
||||
tooltip.style.left = (rect.left - tooltip.offsetWidth - 15) + 'px'; |
||||
} |
||||
if (tooltipRect.top < 0) { |
||||
tooltip.style.top = '10px'; |
||||
} |
||||
if (tooltipRect.bottom > window.innerHeight) { |
||||
tooltip.style.top = (window.innerHeight - tooltip.offsetHeight - 10) + 'px'; |
||||
} |
||||
} |
||||
|
||||
hideTooltip() { |
||||
const tooltip = document.getElementById('custom-tooltip'); |
||||
if (tooltip) { |
||||
tooltip.style.display = 'none'; |
||||
} |
||||
} |
||||
|
||||
async showMessagesModal() { |
||||
const { getAllMessages } = await import("../data/messages.js"); |
||||
const messages = getAllMessages(); |
||||
|
||||
const modal = document.createElement('div'); |
||||
modal.className = 'messages-modal-overlay'; |
||||
modal.innerHTML = ` |
||||
<div class="messages-modal"> |
||||
<div class="messages-modal-header"> |
||||
<h2>Messages for Prime</h2> |
||||
<button class="messages-close-btn" aria-label="Close">×</button> |
||||
</div> |
||||
<div class="messages-modal-content"> |
||||
${messages.length > 0 ? messages.map((msg, index) => ` |
||||
<div class="message-item"> |
||||
<div class="message-from">From: ${msg.from}</div> |
||||
<div class="message-text">${msg.message}</div> |
||||
</div> |
||||
`).join('') : ` |
||||
<div class="no-messages-placeholder"> |
||||
<p>No messages added yet!</p> |
||||
<p>Add your birthday messages to <code>src/data/messages.js</code></p> |
||||
</div> |
||||
`}
|
||||
</div> |
||||
</div> |
||||
`;
|
||||
|
||||
const closeModal = () => { |
||||
modal.remove(); |
||||
}; |
||||
|
||||
// Setup modal events
|
||||
const closeBtn = modal.querySelector('.messages-close-btn'); |
||||
this.on(closeBtn, 'click', closeModal); |
||||
this.on(modal, 'click', (e) => { |
||||
if (e.target === modal) closeModal(); |
||||
}); |
||||
|
||||
document.body.appendChild(modal); |
||||
} |
||||
|
||||
closeTopModal() { |
||||
const modal = document.querySelector('.messages-modal-overlay'); |
||||
if (modal) { |
||||
modal.remove(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Cleanup all event listeners |
||||
*/ |
||||
cleanup() { |
||||
// Remove tracked listeners
|
||||
for (const [element, handlers] of this.listeners) { |
||||
for (const { event, handler, options } of handlers) { |
||||
element.removeEventListener(event, handler, options); |
||||
} |
||||
} |
||||
this.listeners.clear(); |
||||
|
||||
// Clear keyboard handlers
|
||||
this.keyHandlers.clear(); |
||||
} |
||||
|
||||
/** |
||||
* Complete cleanup on destroy |
||||
*/ |
||||
destroy() { |
||||
this.cleanup(); |
||||
|
||||
// Remove global handlers
|
||||
if (this.globalHandlers.has('keydown')) { |
||||
document.removeEventListener('keydown', this.globalKeyHandler); |
||||
} |
||||
if (this.globalHandlers.has('escape')) { |
||||
document.removeEventListener('keydown', this.globalEscapeHandler); |
||||
} |
||||
|
||||
this.globalHandlers.clear(); |
||||
} |
||||
} |
||||
@ -0,0 +1,466 @@
@@ -0,0 +1,466 @@
|
||||
/** |
||||
* InputManager - Centralized event handling for Birthday Spire |
||||
*
|
||||
* This class consolidates ALL event listeners from the render functions |
||||
* into one place while maintaining exact same functionality. |
||||
*
|
||||
* Following Nystrom's Input Handling patterns from Game Programming Patterns |
||||
*/ |
||||
|
||||
export class InputManager { |
||||
constructor(gameRoot) { |
||||
this.root = gameRoot; |
||||
this.activeHandlers = new Map(); // Track active event listeners for cleanup
|
||||
this.globalHandlers = new Set(); // Track global document listeners
|
||||
|
||||
// Bind methods to preserve 'this' context
|
||||
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this); |
||||
this.handleGlobalClick = this.handleGlobalClick.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Initialize global event listeners (always active) |
||||
*/ |
||||
initGlobalListeners() { |
||||
// Global keyboard handling
|
||||
document.addEventListener('keydown', this.handleGlobalKeydown); |
||||
this.globalHandlers.add('keydown'); |
||||
|
||||
// Global click handling for data attributes
|
||||
document.addEventListener('click', this.handleGlobalClick); |
||||
this.globalHandlers.add('click'); |
||||
} |
||||
|
||||
/** |
||||
* Global keyboard event handler |
||||
*/ |
||||
handleGlobalKeydown(event) { |
||||
// Handle Escape key for modals
|
||||
if (event.key === 'Escape') { |
||||
this.handleEscapeKey(event); |
||||
} |
||||
|
||||
// Add other global shortcuts here as needed
|
||||
} |
||||
|
||||
/** |
||||
* Global click handler using event delegation |
||||
*/ |
||||
handleGlobalClick(event) { |
||||
const target = event.target; |
||||
|
||||
// Event delegation for game interactions
|
||||
|
||||
// Handle clicks on elements with data attributes (check both direct and parent elements)
|
||||
|
||||
// Check for card play (battle-card with data-play)
|
||||
const cardElement = target.closest('[data-play]'); |
||||
if (cardElement) { |
||||
this.handleCardPlay(cardElement, event); |
||||
return; // Early return to avoid duplicate handling
|
||||
} |
||||
|
||||
// Check for other interactive elements (using closest to handle child elements)
|
||||
const actionElement = target.closest('[data-action]'); |
||||
if (actionElement) { |
||||
this.handleActionButton(actionElement, event); |
||||
return; |
||||
} |
||||
|
||||
const actElement = target.closest('[data-act]'); |
||||
if (actElement) { |
||||
this.handleRestAction(actElement, event); |
||||
return; |
||||
} |
||||
|
||||
const pickElement = target.closest('[data-pick]'); |
||||
if (pickElement) { |
||||
this.handleRewardPick(pickElement, event); |
||||
return; |
||||
} |
||||
|
||||
const choiceElement = target.closest('[data-choice]'); |
||||
if (choiceElement) { |
||||
this.handleEventChoice(choiceElement, event); |
||||
return; |
||||
} |
||||
|
||||
const upgradeElement = target.closest('[data-upgrade]'); |
||||
if (upgradeElement) { |
||||
this.handleCardUpgrade(upgradeElement, event); |
||||
return; |
||||
} |
||||
|
||||
const buyCardElement = target.closest('[data-buy-card]'); |
||||
if (buyCardElement) { |
||||
this.handleShopCardBuy(buyCardElement, event); |
||||
return; |
||||
} |
||||
|
||||
const relicElement = target.closest('[data-relic]'); |
||||
if (relicElement) { |
||||
this.handleRelicSelection(relicElement, event); |
||||
return; |
||||
} |
||||
|
||||
// Check for direct data attributes on target (fallback)
|
||||
if (target.dataset.node !== undefined) { |
||||
this.handleMapNodeClick(target, event); |
||||
} |
||||
|
||||
// Handle spire node clicks (check if clicked element is inside a spire-node)
|
||||
const spireNode = target.closest('.spire-node'); |
||||
if (spireNode && spireNode.dataset.node) { |
||||
this.handleMapNodeClick(spireNode, event); |
||||
} |
||||
|
||||
// Handle other specific buttons
|
||||
this.handleSpecificButtons(target, event); |
||||
} |
||||
|
||||
/** |
||||
* Handle card play clicks |
||||
*/ |
||||
handleCardPlay(element, event) { |
||||
if (!element.classList.contains('playable')) return; |
||||
|
||||
const index = parseInt(element.dataset.play, 10); |
||||
const card = this.root.player.hand[index]; |
||||
|
||||
if (!card) return; |
||||
if (this.root.player.energy < card.cost) return; |
||||
|
||||
try { |
||||
// Play sound
|
||||
this.playSound('played-card.mp3'); |
||||
|
||||
// Use the root.play method which calls playCard internally
|
||||
this.root.play(index); |
||||
|
||||
// Clear card selection
|
||||
this.root.selectedCardIndex = null; |
||||
if (window.gameModules?.render?.updateCardSelection) { |
||||
window.gameModules.render.updateCardSelection(this.root); |
||||
} |
||||
} catch (error) { |
||||
console.error('Error playing card:', error); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle map node clicks |
||||
*/ |
||||
handleMapNodeClick(element, event) { |
||||
if (!element.dataset.node) return; |
||||
this.root.go(element.dataset.node); |
||||
} |
||||
|
||||
/** |
||||
* Handle reward card picks |
||||
*/ |
||||
handleRewardPick(element, event) { |
||||
const idx = parseInt(element.dataset.pick, 10); |
||||
this.root.takeReward(idx); |
||||
} |
||||
|
||||
/** |
||||
* Handle event choice clicks |
||||
*/ |
||||
handleEventChoice(element, event) { |
||||
const idx = parseInt(element.dataset.choice, 10); |
||||
// Get the current event from the root (this will need to be accessible)
|
||||
if (this.root.currentEvent && this.root.currentEvent.choices[idx]) { |
||||
this.root.currentEvent.choices[idx].effect(); |
||||
this.root.afterNode(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle card upgrade clicks |
||||
*/ |
||||
handleCardUpgrade(element, event) { |
||||
const deckIndex = parseInt(element.dataset.upgrade, 10); |
||||
const oldCardId = this.root.player.deck[deckIndex]; |
||||
|
||||
// Find the upgraded version and replace it
|
||||
const { CARDS } = window.gameModules?.cards || {}; |
||||
if (CARDS && CARDS[oldCardId]?.upgrades) { |
||||
this.root.player.deck[deckIndex] = CARDS[oldCardId].upgrades; |
||||
this.root.log(`Upgraded ${CARDS[oldCardId].name} to ${CARDS[CARDS[oldCardId].upgrades].name}`); |
||||
this.root.afterNode(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle shop card purchases |
||||
*/ |
||||
handleShopCardBuy(element, event) { |
||||
const idx = parseInt(element.dataset.buyCard, 10); |
||||
// This will need access to the current shop cards
|
||||
if (this.root.currentShopCards && this.root.currentShopCards[idx]) { |
||||
const card = this.root.currentShopCards[idx]; |
||||
if (this.root.player.gold >= 50) { |
||||
this.root.player.gold -= 50; |
||||
this.root.player.deck.push(card.id); |
||||
this.root.log(`Bought ${card.name} for 50 gold.`); |
||||
element.disabled = true; |
||||
element.textContent = "SOLD"; |
||||
|
||||
// Update gold display
|
||||
const goldDisplay = this.root.app.querySelector('.gold-amount'); |
||||
if (goldDisplay) { |
||||
goldDisplay.textContent = this.root.player.gold; |
||||
} |
||||
} else { |
||||
this.root.log("Not enough gold!"); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle relic selection |
||||
*/ |
||||
handleRelicSelection(element, event) { |
||||
const relicId = element.dataset.relic; |
||||
this.root.selectStartingRelic(relicId); |
||||
} |
||||
|
||||
/** |
||||
* Handle action buttons (like show-messages, end) |
||||
*/ |
||||
handleActionButton(element, event) { |
||||
const action = element.dataset.action; |
||||
|
||||
switch (action) { |
||||
case 'show-messages': |
||||
this.handleShowMessages(); |
||||
break; |
||||
case 'end': |
||||
this.handleEndTurn(element, event); |
||||
break; |
||||
default: |
||||
console.warn(`Unknown action: ${action}`); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle rest screen actions |
||||
*/ |
||||
handleRestAction(element, event) { |
||||
const action = element.dataset.act; |
||||
|
||||
switch (action) { |
||||
case 'heal': |
||||
const heal = Math.floor(this.root.player.maxHp * 0.2); |
||||
this.root.player.hp = Math.min(this.root.player.maxHp, this.root.player.hp + heal); |
||||
this.root.log(`Healed for ${heal} HP.`); |
||||
this.root.afterNode(); |
||||
break; |
||||
case 'upgrade': |
||||
// This will need to call renderUpgrade
|
||||
if (window.gameModules?.render?.renderUpgrade) { |
||||
window.gameModules.render.renderUpgrade(this.root); |
||||
} |
||||
break; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle end turn button |
||||
*/ |
||||
handleEndTurn(element, event) { |
||||
try { |
||||
this.root.end(); |
||||
|
||||
// Clear card selection
|
||||
this.root.selectedCardIndex = null; |
||||
if (window.gameModules?.render?.updateCardSelection) { |
||||
window.gameModules.render.updateCardSelection(this.root); |
||||
} |
||||
} catch (error) { |
||||
console.error('Error ending turn:', error); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle specific buttons that don't use data attributes |
||||
*/ |
||||
handleSpecificButtons(element, event) { |
||||
// Skip reward button
|
||||
if (element.dataset.skip !== undefined) { |
||||
if (this.root._pendingChoices) { |
||||
this.root.skipReward(); |
||||
} else { |
||||
this.root.afterNode(); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
// Reset button
|
||||
if (element.dataset.reset !== undefined) { |
||||
this.root.clearSave(); |
||||
this.root.reset(); |
||||
return; |
||||
} |
||||
|
||||
// Replay button
|
||||
if (element.dataset.replay !== undefined) { |
||||
this.root.reset(); |
||||
return; |
||||
} |
||||
|
||||
// Menu button
|
||||
if (element.dataset.menu !== undefined) { |
||||
this.root.reset(); |
||||
return; |
||||
} |
||||
|
||||
// Restart Act 2 button
|
||||
if (element.dataset.restartAct2 !== undefined) { |
||||
if (this.root.loadAct2Checkpoint) { |
||||
this.root.loadAct2Checkpoint().then(() => { |
||||
if (window.gameModules?.render?.renderMap) { |
||||
window.gameModules.render.renderMap(this.root); |
||||
} |
||||
}); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
// Buy relic button
|
||||
if (element.dataset.buyRelic !== undefined) { |
||||
if (this.root.currentShopRelic && this.root.player.gold >= 100) { |
||||
this.root.player.gold -= 100; |
||||
this.root.log(`Bought ${this.root.currentShopRelic.name} for 100 gold.`); |
||||
|
||||
// Add relic logic here
|
||||
element.disabled = true; |
||||
element.textContent = "SOLD"; |
||||
} else { |
||||
this.root.log("Not enough gold!"); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
// Leave shop button
|
||||
if (element.dataset.leave !== undefined) { |
||||
this.root.afterNode(); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle Escape key presses |
||||
*/ |
||||
handleEscapeKey(event) { |
||||
// Close any open modals
|
||||
const modals = document.querySelectorAll('.messages-modal-overlay'); |
||||
modals.forEach(modal => modal.remove()); |
||||
} |
||||
|
||||
/** |
||||
* Handle show messages action |
||||
*/ |
||||
async handleShowMessages() { |
||||
try { |
||||
const { getAllMessages } = await import("../data/messages.js"); |
||||
const messages = getAllMessages(); |
||||
|
||||
const modal = document.createElement('div'); |
||||
modal.className = 'messages-modal-overlay'; |
||||
modal.innerHTML = ` |
||||
<div class="messages-modal"> |
||||
<div class="messages-modal-header"> |
||||
<h2>Messages for Prime</h2> |
||||
<button class="messages-close-btn" aria-label="Close">×</button> |
||||
</div> |
||||
<div class="messages-modal-content"> |
||||
${messages.length > 0 ? messages.map((msg, index) => ` |
||||
<div class="message-item"> |
||||
<div class="message-from">From: ${msg.from}</div> |
||||
<div class="message-text">${msg.message}</div> |
||||
</div> |
||||
`).join('') : ` |
||||
<div class="no-messages-placeholder"> |
||||
<p>No messages added yet!</p> |
||||
<p>Add your birthday messages to <code>src/data/messages.js</code></p> |
||||
</div> |
||||
`}
|
||||
</div> |
||||
</div> |
||||
`;
|
||||
|
||||
// Close functionality
|
||||
const closeModal = () => modal.remove(); |
||||
|
||||
const closeBtn = modal.querySelector('.messages-close-btn'); |
||||
closeBtn.addEventListener('click', closeModal); |
||||
|
||||
// Close on overlay click
|
||||
modal.addEventListener('click', (e) => { |
||||
if (e.target === modal) closeModal(); |
||||
}); |
||||
|
||||
document.body.appendChild(modal); |
||||
} catch (error) { |
||||
console.error('Error showing messages:', error); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Setup card hover sound effects |
||||
*/ |
||||
setupCardHoverSounds() { |
||||
// This will be called after battle screen renders
|
||||
this.root.app.querySelectorAll("[data-play]").forEach(btn => { |
||||
if (!btn.dataset.hoverSetup) { |
||||
btn.addEventListener("mouseenter", () => { |
||||
if (btn.classList.contains('playable')) { |
||||
this.playSound('swipe.mp3'); |
||||
} |
||||
}); |
||||
btn.dataset.hoverSetup = 'true'; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Play sound utility |
||||
*/ |
||||
playSound(soundFile) { |
||||
try { |
||||
const audio = new Audio(`assets/sounds/${soundFile}`); |
||||
audio.volume = 0.3; |
||||
audio.play().catch(() => {}); // Ignore audio play failures
|
||||
} catch (e) { |
||||
// Ignore audio errors
|
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Clean up all event listeners |
||||
*/ |
||||
cleanup() { |
||||
// Remove global listeners
|
||||
if (this.globalHandlers.has('keydown')) { |
||||
document.removeEventListener('keydown', this.handleGlobalKeydown); |
||||
} |
||||
if (this.globalHandlers.has('click')) { |
||||
document.removeEventListener('click', this.handleGlobalClick); |
||||
} |
||||
|
||||
this.globalHandlers.clear(); |
||||
this.activeHandlers.clear(); |
||||
} |
||||
} |
||||
|
||||
// Simple audio utility function (moved from render.js)
|
||||
function playSound(soundFile) { |
||||
try { |
||||
const audio = new Audio(`assets/sounds/${soundFile}`); |
||||
audio.volume = 0.3; |
||||
audio.play().catch(() => {}); // Ignore failures in restrictive environments
|
||||
} catch (e) { |
||||
// Audio not supported or file missing, ignore
|
||||
} |
||||
} |
||||
@ -0,0 +1,274 @@
@@ -0,0 +1,274 @@
|
||||
/** |
||||
* Clean render functions without event handling logic |
||||
* Event handling is now managed by the EventHandler class |
||||
*/ |
||||
|
||||
// Simple audio utility (kept for compatibility)
|
||||
function playSound(soundFile) { |
||||
try { |
||||
const audio = new Audio(`assets/sounds/${soundFile}`); |
||||
audio.volume = 0.3; |
||||
audio.play().catch(e => { console.log(e) }); |
||||
} catch (e) { |
||||
// Silently fail if audio not available
|
||||
} |
||||
} |
||||
|
||||
export function showDamageNumber(damage, target, isPlayer = false) { |
||||
const targetElement = isPlayer ? |
||||
document.querySelector('.player-battle-zone') : |
||||
document.querySelector('.enemy-battle-zone'); |
||||
|
||||
if (!targetElement) return; |
||||
|
||||
const damageNumber = document.createElement('div'); |
||||
damageNumber.className = 'damage-number'; |
||||
damageNumber.textContent = damage; |
||||
|
||||
const rect = targetElement.getBoundingClientRect(); |
||||
damageNumber.style.left = `${rect.left + rect.width / 2}px`; |
||||
damageNumber.style.top = `${rect.top + rect.height / 2}px`; |
||||
|
||||
document.body.appendChild(damageNumber); |
||||
|
||||
requestAnimationFrame(() => { |
||||
damageNumber.classList.add('damage-number-animate'); |
||||
}); |
||||
|
||||
setTimeout(() => { |
||||
if (damageNumber.parentNode) { |
||||
damageNumber.parentNode.removeChild(damageNumber); |
||||
} |
||||
}, 1000); |
||||
} |
||||
|
||||
export async function renderBattleClean(root) { |
||||
const app = root.app; |
||||
const p = root.player, e = root.enemy; |
||||
|
||||
const { ENEMIES } = await import("../data/enemies.js"); |
||||
const { CARDS } = await import("../data/cards.js"); |
||||
const { RELICS } = await import("../data/relics.js"); |
||||
const enemyData = ENEMIES[e.id]; |
||||
const backgroundImage = enemyData?.background || null; |
||||
|
||||
const intentInfo = { |
||||
attack: { emoji: '', text: `Will attack for ${e.intent.value} damage`, color: 'danger' }, |
||||
block: { emoji: '', text: `Will gain ${e.intent.value} block`, color: 'info' }, |
||||
debuff: { emoji: '', text: 'Will apply a debuff', color: 'warning' }, |
||||
heal: { emoji: '', text: `Will heal for ${e.intent.value} HP`, color: 'success' } |
||||
}[e.intent.type] || { emoji: '', text: 'Unknown intent', color: 'neutral' }; |
||||
|
||||
app.innerHTML = ` |
||||
<div class="battle-scene"> |
||||
<!-- Battle Arena with background --> |
||||
<div class="battle-arena" ${backgroundImage ? `style="background-image: url('${backgroundImage}'); background-size: cover; background-position: center; background-repeat: no-repeat;"` : ''}> |
||||
|
||||
<!-- Enemy Section --> |
||||
<div class="enemy-battle-zone"> |
||||
<div class="enemy-container"> |
||||
<div class="enemy-character"> |
||||
<div class="enemy-sprite"> |
||||
<div class="enemy-avatar">${getEnemyArt(e.id, ENEMIES)}</div> |
||||
<div class="enemy-shadow"></div> |
||||
${e.block > 0 ? `<div class="shield-effect"><img src="assets/card-art/shield.png" alt="Shield" class="shield-effect-img"></div>` : ''} |
||||
${e.weak > 0 ? `<div class="debuff-effect"><img src="assets/card-art/heart_damaged.png" alt="Weak" class="debuff-effect-img"></div>` : ''} |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="enemy-ui-panel"> |
||||
<div class="enemy-nameplate"> |
||||
<h2 class="enemy-title">${e.name}</h2> |
||||
<div class="enemy-level">${getEnemyType(e.id)}</div> |
||||
</div> |
||||
<div class="enemy-health-section"> |
||||
<div class="health-bar-container"> |
||||
<div class="health-bar enemy-health"> |
||||
<div class="health-fill" style="width: ${(e.hp / e.maxHp) * 100}%"></div> |
||||
<div class="health-text">${e.hp} / ${e.maxHp}</div> |
||||
<div class="health-glow"></div> |
||||
</div> |
||||
</div> |
||||
${e.block > 0 ? ` |
||||
<div class="status-effect block-status"> |
||||
<img src="assets/card-art/shield.png" alt="Block" class="status-icon-img"> |
||||
<span class="status-value">${e.block}</span> |
||||
<span class="status-label">Block</span> |
||||
</div> |
||||
` : ''}
|
||||
</div> |
||||
|
||||
<div class="intent-panel intent-${intentInfo.color}"> |
||||
<div class="intent-header"> |
||||
<span class="intent-label">Next Action</span> |
||||
</div> |
||||
<div class="intent-content"> |
||||
<div class="intent-icon-large">${intentInfo.emoji}</div> |
||||
<div class="intent-description">${intentInfo.text}</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Player Section --> |
||||
<div class="player-battle-zone"> |
||||
<div class="player-container"> |
||||
<div class="player-character"> |
||||
<div class="player-sprite"> |
||||
<div class="player-avatar"> |
||||
<img src="assets/prime.webp" alt="Prime" class="player-avatar-img" /> |
||||
</div> |
||||
<div class="player-shadow"></div> |
||||
${p.block > 0 ? `<div class="shield-effect"><img src="assets/card-art/shield.png" alt="Shield" class="shield-effect-img"></div>` : ''} |
||||
${p.weak > 0 ? `<div class="debuff-effect"><img src="assets/card-art/heart_damaged.png" alt="Weak" class="debuff-effect-img"></div>` : ''} |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="player-ui-panel"> |
||||
<div class="player-nameplate"> |
||||
<h2 class="player-title">ThePrimeagen</h2> |
||||
<div class="player-level">PLAYER</div> |
||||
</div> |
||||
|
||||
<div class="player-health-section"> |
||||
<div class="health-bar-container"> |
||||
<div class="health-bar player-health"> |
||||
<div class="health-fill" style="width: ${(p.hp / p.maxHp) * 100}%"></div> |
||||
<div class="health-text">${p.hp} / ${p.maxHp}</div> |
||||
<div class="health-glow"></div> |
||||
</div> |
||||
</div> |
||||
${p.block > 0 ? ` |
||||
<div class="status-effect block-status"> |
||||
<img src="assets/card-art/shield.png" alt="Block" class="status-icon-img"> |
||||
<span class="status-value">${p.block}</span> |
||||
<span class="status-label">Block</span> |
||||
</div> |
||||
` : ''}
|
||||
${p.weak > 0 ? ` |
||||
<div class="status-effect weak-status"> |
||||
<img src="assets/card-art/heart_damaged.png" alt="Weak" class="status-icon-img"> |
||||
<span class="status-value">${p.weak}</span> |
||||
<span class="status-label">Weak</span> |
||||
</div> |
||||
` : ''}
|
||||
</div> |
||||
|
||||
<div class="player-energy-section"> |
||||
<div class="energy-display"> |
||||
<span class="energy-label">⚡</span> |
||||
<div class="energy-orbs"> |
||||
${Array.from({ length: p.maxEnergy }, (_, i) => |
||||
`<div class="energy-orb ${i < p.energy ? 'active' : 'inactive'}"></div>` |
||||
).join('')} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Battle Action Zone --> |
||||
<div class="battle-action-zone"> |
||||
<div class="hand-area"> |
||||
<div class="hand-header"> |
||||
<div class="deck-counters"></div> |
||||
</div> |
||||
|
||||
<div class="cards-battlefield"> |
||||
${p.hand.length === 0 ? |
||||
'<div class="no-cards-message">🎴 No cards in hand - End turn to draw new cards</div>' : |
||||
p.hand.map((card, i) => { |
||||
const canPlay = p.energy >= card.cost; |
||||
const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power'; |
||||
return ` |
||||
<div class="battle-card ${cardType} ${!canPlay ? 'unplayable' : 'playable'}" data-play="${i}"> |
||||
<div class="card-glow"></div> |
||||
<div class="card-frame"> |
||||
<div class="card-header-row"> |
||||
<div class="card-title">${card.name}</div> |
||||
<div class="card-cost-orb ${!canPlay ? 'insufficient' : ''}">${card.cost}</div> |
||||
</div> |
||||
|
||||
<div class="card-artwork"> |
||||
<div class="card-art-icon">${getCardArt(card.id, CARDS)}</div> |
||||
<div class="card-type-badge ${cardType}">${card.type}</div> |
||||
</div> |
||||
|
||||
<div class="card-description-box"> |
||||
<div class="card-text">${card.text}</div> |
||||
</div> |
||||
</div> |
||||
${!canPlay ? `<div class="card-disabled-overlay"><span>Need ${card.cost} energy</span></div>` : ''} |
||||
</div> |
||||
`;
|
||||
}).join('') |
||||
} |
||||
</div> |
||||
|
||||
<div class="hand-controls"> |
||||
<button class="end-turn-btn" data-action="end"> |
||||
<span class="end-turn-text">End Turn</span> |
||||
<span class="end-turn-hotkey">E</span> |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- Fight Log Panel --> |
||||
<div class="fight-log-panel"> |
||||
<div class="fight-log-header"> |
||||
<span class="fight-log-title">Combat Log</span> |
||||
</div> |
||||
<div class="fight-log-content" id="fight-log-content"> |
||||
${root.logs.slice(-20).map(log => `<div class="log-entry">${log}</div>`).join('')} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
`;
|
||||
|
||||
// Initialize card selection state if not exists
|
||||
if (root.selectedCardIndex === undefined) { |
||||
root.selectedCardIndex = null; |
||||
} |
||||
|
||||
// Auto-scroll fight log to bottom
|
||||
const logContent = document.getElementById('fight-log-content'); |
||||
if (logContent) { |
||||
logContent.scrollTop = logContent.scrollHeight; |
||||
} |
||||
|
||||
// Note: Event handling is now managed by EventHandler class
|
||||
// No addEventListener calls needed here!
|
||||
} |
||||
|
||||
// Utility functions (kept for compatibility)
|
||||
function getRelicArt(relicId, RELICS = null) { |
||||
if (RELICS && RELICS[relicId]?.art) { |
||||
const imagePath = RELICS[relicId].art; |
||||
return `<img src="assets/skill-art/${imagePath}" alt="${relicId}" class="relic-skill-art">`; |
||||
} |
||||
return '💎'; |
||||
} |
||||
|
||||
function getCardArt(cardId, CARDS = null) { |
||||
if (CARDS && CARDS[cardId]?.art) { |
||||
const imagePath = CARDS[cardId].art; |
||||
return `<img src="assets/skill-art/${imagePath}" alt="${cardId}" class="card-art-image">`; |
||||
} |
||||
return `<span>🃏</span>`; |
||||
} |
||||
|
||||
function getEnemyArt(enemyId, ENEMIES = null) { |
||||
const enemyData = ENEMIES?.[enemyId]; |
||||
const avatarPath = enemyData?.avatar || `assets/avatars/${enemyId}.png`; |
||||
return `<img src="${avatarPath}" alt="${enemyId}" class="enemy-avatar-img">`; |
||||
} |
||||
|
||||
function getEnemyType(enemyId) { |
||||
if (enemyId.includes('boss_')) return 'BOSS'; |
||||
if (enemyId.includes('elite_')) return 'ELITE'; |
||||
return 'ENEMY'; |
||||
} |
||||
Loading…
Reference in new issue