Files
Aquarium/index.html
T
kamilkrzywda 0f360d76cd fix: runtime Box3 scaling + Alpine null-safety
- Replaced hardcoded SPECIES_LENGTH with runtime Box3 measurement
  (model added to parent group BEFORE measuring, so full GLB
  hierarchy transforms are included in world matrices)
- All 9 species now render at correct target size (0.13-0.32u)
- Added optional chaining (?.) to all Alpine template expressions
  referencing store.selectedFish to prevent null-reference errors
- Guarded getFishValue/canBreed calls with null checks
2026-10-08 17:03:37 +00:00

193 lines
10 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Aquarium</title>
</head>
<body class="bg-aqua-900">
<canvas id="aquarium" class="fixed inset-0 w-full h-full"></canvas>
<!-- UI Overlay -->
<div id="game-ui" class="fixed inset-0 pointer-events-none" x-data="gameUI()" x-init="init()">
<!-- Top HUD Bar -->
<div class="absolute top-0 left-0 right-0 h-12 flex items-center justify-between px-4 pointer-events-auto bg-aqua-900/70 backdrop-blur-sm border-b border-aqua-700/50">
<!-- Left: Money + Fish count -->
<div class="flex items-center gap-4">
<div class="flex items-center gap-1.5 text-yellow-400 font-semibold">
<span class="text-lg">$</span>
<span x-text="formatMoney(store.money)"></span>
</div>
<div class="flex items-center gap-1.5 text-aqua-200">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6.5 12c.94-3.46 4.94-6 8.5-6 3.56 0 7.56 2.54 8.5 6-.94 3.47-4.94 6-8.5 6-3.56 0-7.56-2.53-8.5-6Z"/><path d="M6 12H2"/><path d="M6 12l3-3"/><path d="M6 12l3 3"/></svg>
<span x-text="store.fishCount + ' / ' + store.fishCapacity"></span>
</div>
</div>
<!-- Right: Tank tier + buttons -->
<div class="flex items-center gap-2">
<span class="text-xs text-aqua-300 px-2 py-0.5 rounded bg-aqua-800/80 border border-aqua-700" x-text="store.tankName + ' Tank'"></span>
<button @click="store.showShop = true"
class="text-xs px-3 py-1.5 rounded-lg bg-aqua-700 hover:bg-aqua-600 text-white transition-colors cursor-pointer">
🐟 Shop
</button>
<button @click="store.showSettings = true"
class="text-xs px-2 py-1.5 rounded-lg bg-aqua-800 hover:bg-aqua-700 text-aqua-300 transition-colors cursor-pointer">
⚙️
</button>
</div>
</div>
<!-- Fish Info Panel (right side) -->
<div x-if="store.selectedFish" x-transition.opacity.duration.200ms
class="absolute right-4 top-16 w-72 pointer-events-auto"
x-cloak>
<div class="bg-aqua-900/90 backdrop-blur-md rounded-xl border border-aqua-700/60 p-4 shadow-xl">
<!-- Header -->
<div class="flex items-start justify-between mb-3">
<div>
<h3 class="text-white font-semibold text-sm" x-text="store.selectedFish?.name"></h3>
<div class="flex items-center gap-1.5 mt-1">
<span class="text-[10px] px-1.5 py-0.5 rounded font-medium"
:class="rarityBg(store.selectedFish?.genetics.rarity)"
x-text="store.selectedFish?.genetics.rarity.toUpperCase()"></span>
<span class="text-[10px] text-aqua-300" x-text="'(' + rarityMult(store.selectedFish?.genetics.rarity) + 'x value)'"></span>
</div>
</div>
<button @click="store.selectedFish = null" class="text-aqua-400 hover:text-white cursor-pointer">✕</button>
</div>
<!-- Genetics -->
<div class="grid grid-cols-2 gap-x-3 gap-y-1.5 text-xs mb-3">
<div class="text-aqua-300">Gender</div>
<div class="text-white" x-text="store.selectedFish?.genetics.gender === 'male' ? '♂ Male' : '♀ Female'"></div>
<div class="text-aqua-300">Size</div>
<div class="text-white" x-text="store.selectedFish?.genetics.sizeMult.toFixed(2) + 'x'"></div>
<div class="text-aqua-300">Speed</div>
<div class="text-white" x-text="store.selectedFish?.genetics.speedMult.toFixed(2) + 'x'"></div>
<div class="text-aqua-300">Pattern</div>
<div class="text-white capitalize" x-text="store.selectedFish?.genetics.pattern"></div>
<div class="text-aqua-300">Age</div>
<div class="text-white" x-text="formatAge(store.selectedFish?.age)"></div>
</div>
<!-- Value -->
<div class="text-sm mb-3">
<span class="text-aqua-300">Sell value: </span>
<span class="text-yellow-400 font-bold" x-text="store.selectedFish ? formatMoney(getFishValue(store.selectedFish)) : ''"></span>
</div>
<!-- Actions -->
<div class="flex gap-2">
<button @click="requestSell()"
class="flex-1 text-xs py-2 rounded-lg bg-red-900/60 hover:bg-red-800 text-red-200 border border-red-800 transition-colors cursor-pointer">
Sell
</button>
<button @click="requestBreed()"
class="flex-1 text-xs py-2 rounded-lg bg-aqua-700 hover:bg-aqua-600 text-white border border-aqua-600 transition-colors cursor-pointer"
x-disabled="!store.selectedFish || !canBreed(store.selectedFish)">
Breed →
</button>
</div>
</div>
</div>
<!-- Shop Modal -->
<div x-show="store.showShop" x-transition.opacity.duration.200ms
class="absolute inset-0 flex items-center justify-center pointer-events-auto bg-black/60 backdrop-blur-sm"
x-cloak>
<div class="bg-aqua-900/95 border border-aqua-700 rounded-2xl w-[500px] max-h-[80vh] overflow-y-auto p-6 shadow-2xl">
<div class="flex items-center justify-between mb-4">
<h2 class="text-white font-bold text-lg">🐟 Fish Shop</h2>
<button @click="store.showShop = false" class="text-aqua-400 hover:text-white cursor-pointer text-xl">✕</button>
</div>
<!-- Fish grid -->
<div class="grid grid-cols-2 gap-3 mb-4">
<template x-for="sp in shopSpecies" :key="sp">
<div class="bg-aqua-800/60 rounded-xl border border-aqua-700/50 p-3">
<div class="text-white font-medium text-sm mb-1" x-text="speciesNames[sp]"></div>
<div class="text-yellow-400 text-xs mb-2" x-text="formatMoney(fishPrices[sp])"></div>
<div class="text-[10px] text-aqua-400 mb-2 leading-tight" x-html="rarityOdds(sp)"></div>
<button @click="buyFish(sp)"
:disabled="store.money < fishPrices[sp] || store.fishCount >= store.fishCapacity"
class="w-full text-xs py-1.5 rounded-lg bg-aqua-600 hover:bg-aqua-500 text-white transition-colors cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed">
<span x-text="store.fishCount >= store.fishCapacity ? 'Tank Full' : 'Buy'"></span>
</button>
</div>
</template>
</div>
<!-- Footer -->
<div class="flex justify-between text-xs text-aqua-400">
<span x-text="'Fish: ' + store.fishCount + ' / ' + store.fishCapacity"></span>
<span x-text="'Balance: ' + formatMoney(store.money)"></span>
</div>
</div>
</div>
<!-- Settings Modal -->
<div x-show="store.showSettings" x-transition.opacity.duration.200ms
class="absolute inset-0 flex items-center justify-center pointer-events-auto bg-black/60 backdrop-blur-sm"
x-cloak>
<div class="bg-aqua-900/95 border border-aqua-700 rounded-2xl w-80 p-6 shadow-2xl">
<div class="flex items-center justify-between mb-4">
<h2 class="text-white font-bold">⚙️ Settings</h2>
<button @click="store.showSettings = false" class="text-aqua-400 hover:text-white cursor-pointer">✕</button>
</div>
<!-- Tank upgrade -->
<div class="mb-4" x-show="store.nextTierCost !== null">
<div class="text-xs text-aqua-300 mb-2">Tank: <span x-text="store.tankName"></span></div>
<button @click="upgradeTank()"
:disabled="store.money < store.nextTierCost"
class="w-full text-xs py-2 rounded-lg bg-aqua-600 hover:bg-aqua-500 text-white transition-colors cursor-pointer disabled:opacity-40">
<span x-text="'Upgrade to ' + nextTierName() + ' — $' + store.nextTierCost"></span>
</button>
</div>
<div class="text-xs text-aqua-400 mb-4" x-show="store.nextTierCost === null">
Tank is at MAX size 🎉
</div>
<!-- Reset -->
<button @click="store.showResetConfirm = true"
class="w-full text-xs py-2 rounded-lg bg-red-900/40 hover:bg-red-900/60 text-red-300 border border-red-900 transition-colors cursor-pointer">
Reset Game
</button>
</div>
</div>
<!-- Reset Confirmation -->
<div x-show="store.showResetConfirm" x-transition.opacity.duration.150ms
class="absolute inset-0 flex items-center justify-center pointer-events-auto bg-black/70 z-50"
x-cloak>
<div class="bg-aqua-900 border border-red-900 rounded-2xl w-80 p-6 shadow-2xl">
<h3 class="text-white font-bold mb-2">⚠️ Reset Game</h3>
<p class="text-sm text-aqua-300 mb-4">This will permanently delete ALL your fish, money, and progress.</p>
<p class="text-xs text-red-400 mb-4">Type <code class="bg-red-900/50 px-1 rounded">DELETE</code> to confirm:</p>
<input type="text" x-model="store.resetConfirmText"
class="w-full mb-4 px-3 py-2 rounded-lg bg-aqua-800 border border-aqua-700 text-white text-sm"
placeholder="DELETE" @keyup.enter="confirmReset()" />
<div class="flex gap-2">
<button @click="store.showResetConfirm = false; store.resetConfirmText = ''"
class="flex-1 text-xs py-2 rounded-lg bg-aqua-800 hover:bg-aqua-700 text-white cursor-pointer">Cancel</button>
<button @click="confirmReset()" :disabled="store.resetConfirmText !== 'DELETE'"
class="flex-1 text-xs py-2 rounded-lg bg-red-800 hover:bg-red-700 text-white cursor-pointer disabled:opacity-40">Reset</button>
</div>
</div>
</div>
<!-- Toasts -->
<div class="absolute top-14 right-4 flex flex-col gap-2 items-end">
<template x-for="toast in store.toasts" :key="toast.id">
<div class="px-3 py-2 rounded-lg text-xs font-medium shadow-lg backdrop-blur-sm border transition-all duration-300"
:class="{
'bg-green-900/80 border-green-700 text-green-200': toast.type === 'success',
'bg-aqua-800/80 border-aqua-600 text-aqua-200': toast.type === 'info',
'bg-yellow-900/80 border-yellow-700 text-yellow-200': toast.type === 'warning',
'bg-purple-900/80 border-purple-700 text-purple-200': toast.type === 'rare',
}"
x-text="toast.text">
</div>
</template>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>