+
+ +
+
+

Happy Birthday Prime!

+

With coffee in hand and code on your side,
+The Primeagen Spire’s a treacherous ride.
+Gremlins await and errors conspire,
+But cake lies ahead at the top of the Spire.

+
+ +
+

How to Navigate the Spire

+
    +
  • Click a node to climb the way
  • +
  • Choose your battles night or day
  • +
  • Rest at fires, heal or train
  • +
  • Each new card will grow your gain.
  • +
  • At the summit face the fight
  • +
  • Defeat the boss, win the night
  • +
+ +
+

May your code be bug-free and your coffee stay hot, +May this birthday bring joy in each moment you’ve got.

+
+
+
+ +
+ +
+
Legend
+
Rest Rest
+
Battle Enemy
+
Battle Elite
+
Battle Boss
+
Event Events
+
Shop Shop
+
+ + + ${(() => { + + const nodePositions = { + 'n1': { x: 500, y: 720 }, // Start at bottom center - moved up slightly + 'n2': { x: 350, y: 650 }, // Battle - left branch - moved right and up + 'n3': { x: 650, y: 650 }, // Event - right branch - moved left and up + 'n4': { x: 500, y: 540 }, // Battle - converge - moved up slightly + 'n5': { x: 350, y: 400 }, // Rest - left + 'n6': { x: 650, y: 400 }, // Shop - right + 'n7': { x: 500, y: 300 }, // Battle - converge + 'n8': { x: 500, y: 130 }, // Elite + 'n9': { x: 500, y: 70 }, // Rest + 'n10': { x: 500, y: 20 } // Boss at top + }; + + return m.nodes.map(node => { + if (!node.next || node.next.length === 0) return ''; + + return node.next.map(nextId => { + const fromPos = nodePositions[node.id]; + const toPos = nodePositions[nextId]; + if (!fromPos || !toPos) return ''; + + const isActivePath = (node.id === currentId && nextIds.includes(nextId)) || + (parseInt(nextId.replace('n', '')) <= parseInt(currentId.replace('n', ''))); + + return ``; + }).join(''); + }).join(''); + })()} + + +
+ ${(() => { + + const nodePositions = { + 'n1': { x: 500, y: 720 }, + 'n2': { x: 360, y: 650 }, + 'n3': { x: 630, y: 650 }, + 'n4': { x: 500, y: 540 }, + 'n5': { x: 360, y: 400 }, + 'n6': { x: 630, y: 400 }, + 'n7': { x: 500, y: 300 }, + 'n8': { x: 500, y: 210 }, + 'n9': { x: 500, y: 120 }, + 'n10': { x: 500, y: 40 } + }; + + return m.nodes.map(n => { + const isNext = nextIds.includes(n.id); + const isCurrent = n.id === currentId; + const isCompleted = root.completedNodes.includes(n.id); + const locked = (!isNext && !isCurrent && !isCompleted); + + const pos = nodePositions[n.id]; + if (!pos) return ''; + + const leftPercent = (pos.x / 1000) * 100; + const topPercent = (pos.y / 800) * 100; + const tooltipData = getNodeTooltipData(n); + + return ` +
+
+
+
${getNodeEmoji(n.kind)}
+
+ ${isCurrent ? '
' : ''} +
+ `; + }).join(''); + })()} +
+
+
+ +
+
+ Your deck +
+
+ ${Object.entries( + root.player.deck.reduce((acc, cardId) => { + acc[cardId] = (acc[cardId] || 0) + 1; + return acc; + }, {}) + ).map(([cardId, count], index) => { + const card = CARDS[cardId]; + if (!card) return ''; + + const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power'; + + return ` +
+
+
+
${card.name}
+
${card.cost}
+
+
${getCardArt(cardId)}
+
+
${card.text}
+
+ ${count > 1 ? `
×${count}
` : ''} +
+
+ `; + }).join('')} +
+
+