abyssalfarm.html

Créé Le diff n'expire jamais
1 suppression
1 ligne
929 ajouts
929 lignes
1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>ABYSSAL FARM — Void Breeder</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=VT323&display=swap');

:root {
--bg: #0a0a0f;
--panel: #12121a;
--panel2: #1a1a25;
--accent: #8b0000;
--accent2: #4a0080;
--accent3: #1a5c1a;
--text: #c0c0c0;
--text-dim: #555;
--border: #2a2a35;
--glow-red: 0 0 8px #8b0000;
--glow-purple: 0 0 8px #4a0080;
--glow-green: 0 0 8px #1a5c1a;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
margin: 0; padding: 0;
background: var(--bg);
color: var(--text);
font-family: 'VT323', monospace;
font-size: 18px;
overflow: hidden;
height: 100vh;
width: 100vw;
touch-action: manipulation;
}

/* CRT overlay */
body::before {
content: "";
position: fixed; top:0; left:0; right:0; bottom:0;
background: repeating-linear-gradient(
0deg,
rgba(0,0,0,0.15),
rgba(0,0,0,0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 9999;
}
body::after {
content: "";
position: fixed; top:0; left:0; right:0; bottom:0;
background: radial-gradient(circle, transparent 60%, rgba(0,0,0,0.6) 100%);
pointer-events: none;
z-index: 9998;
}

#app {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 600px;
margin: 0 auto;
position: relative;
z-index: 1;
}

/* Header */
.header {
background: var(--panel);
border-bottom: 2px solid var(--border);
padding: 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}

.header h1 {
margin: 0;
font-size: 1.4rem;
color: var(--accent);
text-shadow: var(--glow-red);
letter-spacing: 2px;
}

.resources {
display: flex;
gap: 12px;
font-size: 0.95rem;
}

.res-item {
display: flex;
align-items: center;
gap: 4px;
}

.res-icon {
width: 14px; height: 14px;
display: inline-block;
}

/* Tabs */
.tabs {
display: flex;
background: var(--panel);
border-bottom: 2px solid var(--border);
flex-shrink: 0;
}

.tab {
flex: 1;
padding: 10px;
text-align: center;
cursor: pointer;
border: none;
background: transparent;
color: var(--text-dim);
font-family: 'VT323', monospace;
font-size: 1rem;
border-bottom: 3px solid transparent;
transition: all 0.2s;
}

.tab.active {
color: var(--text);
border-bottom-color: var(--accent);
background: var(--panel2);
}

.tab:hover { color: var(--text); }

/* Content area */
.content {
flex: 1;
overflow-y: auto;
padding: 12px;
position: relative;
}

.content::-webkit-scrollbar { width: 6px; }
.content::-webkit-scrollbar-thumb { background: var(--accent); }

/* Monster Cards */
.monster-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 10px;
}

.monster-card {
background: var(--panel);
border: 2px solid var(--border);
border-radius: 4px;
padding: 8px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
position: relative;
overflow: hidden;
}

.monster-card:hover {
border-color: var(--accent);
box-shadow: var(--glow-red);
}

.monster-card.selected {
border-color: var(--accent2);
box-shadow: var(--glow-purple);
}

.monster-canvas {
width: 64px; height: 64px;
image-rendering: pixelated;
margin: 0 auto 6px;
display: block;
}

.monster-name {
font-size: 0.9rem;
color: var(--accent);
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.monster-lvl {
font-size: 0.8rem;
color: var(--text-dim);
}

.monster-bar {
width: 100%; height: 4px;
background: #222;
margin-top: 4px;
border-radius: 2px;
overflow: hidden;
}

.monster-bar-fill {
height: 100%;
background: var(--accent);
transition: width 0.3s;
}

/* Hatch Button */
.hatch-area {
text-align: center;
padding: 20px;
margin-bottom: 16px;
}

.hatch-btn {
width: 120px; height: 120px;
border-radius: 50%;
border: 3px solid var(--accent);
background: var(--panel);
color: var(--accent);
font-family: 'VT323', monospace;
font-size: 1.2rem;
cursor: pointer;
position: relative;
overflow: hidden;
box-shadow: var(--glow-red);
transition: transform 0.1s;
display: inline-flex;
align-items: center;
justify-content: center;
flex-direction: column;
}

.hatch-btn:active { transform: scale(0.92); }

.hatch-btn .cost {
font-size: 0.8rem;
color: var(--text-dim);
margin-top: 4px;
}

.hatch-btn::before {
content: "";
position: absolute;
width: 80%; height: 80%;
border: 2px dashed var(--accent);
border-radius: 50%;
animation: spin 8s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Upgrades / Automation */
.upgrade-list {
display: flex;
flex-direction: column;
gap: 8px;
}

.upgrade-item {
background: var(--panel);
border: 2px solid var(--border);
border-radius: 4px;
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
transition: all 0.2s;
}

.upgrade-item:hover { border-color: var(--accent2); }

.upgrade-item.disabled {
opacity: 0.4;
cursor: not-allowed;
}

.upgrade-info { flex: 1; }
.upgrade-name { font-size: 1rem; color: var(--accent2); }
.upgrade-desc { font-size: 0.8rem; color: var(--text-dim); }
.upgrade-cost {
background: var(--panel2);
padding: 4px 10px;
border-radius: 4px;
border: 1px solid var(--border);
font-size: 0.9rem;
white-space: nowrap;
}

/* Farm Zones */
.zone-list {
display: flex;
flex-direction: column;
gap: 10px;
}

.zone-card {
background: var(--panel);
border: 2px solid var(--border);
border-radius: 4px;
padding: 12px;
cursor: pointer;
position: relative;
}

.zone-card:hover { border-color: var(--accent3); }

.zone-name { font-size: 1.1rem; color: var(--accent3); margin-bottom: 4px; }
.zone-desc { font-size: 0.85rem; color: var(--text-dim); }
.zone-reward { font-size: 0.9rem; color: var(--text); margin-top: 6px; }
.zone-danger {
position: absolute;
top: 8px; right: 8px;
font-size: 0.75rem;
color: var(--accent);
}

/* Detail Modal */
.modal-overlay {
position: fixed;
top:0; left:0; right:0; bottom:0;
background: rgba(0,0,0,0.85);
display: none;
align-items: center;
justify-content: center;
z-index: 10000;
padding: 20px;
}

.modal-overlay.active { display: flex; }

.modal {
background: var(--panel);
border: 2px solid var(--accent);
border-radius: 8px;
padding: 20px;
max-width: 400px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
position: relative;
}

.modal-close {
position: absolute;
top: 8px; right: 12px;
background: none;
border: none;
color: var(--text);
font-size: 1.5rem;
cursor: pointer;
font-family: 'VT323', monospace;
}

.modal-title {
font-size: 1.3rem;
color: var(--accent);
margin-bottom: 12px;
text-align: center;
}

.modal-canvas {
width: 96px; height: 96px;
image-rendering: pixelated;
margin: 0 auto 12px;
display: block;
}

.stat-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid var(--border);
font-size: 0.95rem;
}

.action-btn {
width: 100%;
padding: 10px;
margin-top: 10px;
border: 2px solid var(--accent);
background: var(--panel2);
color: var(--text);
font-family: 'VT323', monospace;
font-size: 1rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s;
}

.action-btn:hover { background: var(--accent); color: #fff; }
.action-btn:disabled { opacity: 0.3; cursor: not-allowed; }

/* Floating text */
.float-text {
position: absolute;
color: var(--accent);
font-size: 1rem;
pointer-events: none;
animation: floatUp 1s ease-out forwards;
z-index: 100;
}

@keyframes floatUp {
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-40px); }
}

/* Log */
.log-area {
background: var(--panel);
border: 2px solid var(--border);
border-radius: 4px;
padding: 8px;
height: 120px;
overflow-y: auto;
font-size: 0.85rem;
color: var(--text-dim);
}

.log-entry { margin-bottom: 2px; }
.log-entry.new { color: var(--text); }

/* Glitch effect on title */
.glitch {
animation: glitch 3s infinite;
}

@keyframes glitch {
0%, 90%, 100% { text-shadow: var(--glow-red); }
92% { text-shadow: 2px 0 #0ff, -2px 0 #f0f; }
94% { text-shadow: -2px 0 #0ff, 2px 0 #f0f; }
96% { text-shadow: var(--glow-red); }
}

/* Notification toast */
.toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
background: var(--panel);
border: 2px solid var(--accent);
padding: 10px 20px;
border-radius: 4px;
font-size: 1rem;
z-index: 10001;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
white-space: nowrap;
}

.toast.show { opacity: 1; }

/* Evolution tree */
.evo-tree {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin: 12px 0;
flex-wrap: wrap;
}

.evo-node {
width: 48px; height: 48px;
border: 2px solid var(--border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
color: var(--text-dim);
background: var(--panel2);
}

.evo-node.active {
border-color: var(--accent);
color: var(--accent);
box-shadow: var(--glow-red);
}

.evo-arrow { color: var(--text-dim); font-size: 1.2rem; }

/* Particle canvas */
#particleCanvas {
position: fixed;
top: 0; left: 0;
pointer-events: none;
z-index: 9997;
}

/* Responsive */
@media (max-width: 400px) {
.monster-grid { grid-template-columns: repeat(2, 1fr); }
.header h1 { font-size: 1.1rem; }
}
</style>
</head>
<body>
<canvas id="particleCanvas"></canvas>

<div id="app">
<div class="header">
<h1 class="glitch">ABYSSAL FARM</h1>
<div class="resources">
<div class="res-item"><span style="color:#8b0000">◆</span> <span id="resDread">0</span></div>
<div class="res-item"><span style="color:#4a0080">◆</span> <span id="resSouls">0</span></div>
<div class="res-item"><span style="color:#1a5c1a">◆</span> <span id="resShards">0</span></div>
</div>
</div>

<div class="tabs">
<button class="tab active" data-tab="hatch">HATCH</button>
<button class="tab" data-tab="monsters">MONSTERS</button>
<button class="tab" data-tab="upgrades">VOID SHOP</button>
<button class="tab" data-tab="zones">REALMS</button>
</div>

<div class="content" id="content">
<!-- Dynamic content -->
</div>

<div class="log-area" id="logArea">
<div class="log-entry">Welcome to the Abyss. Hatch your first abomination.</div>
</div>
</div>

<!-- Modal -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal" id="modal">
<button class="modal-close" onclick="closeModal()">×</button>
<div id="modalContent"></div>
</div>
</div>

<div class="toast" id="toast"></div>

<script>
// ==================== GAME STATE ====================
const state = {
dread: 0,
souls: 0,
shards: 0,
totalDread: 0,
monsters: [],
nextId: 1,
selectedMonster: null,
tab: 'hatch',
upgrades: {
extractor: { level: 0, cost: 50, costScale: 1.6, name: "Void Extractor", desc: "Auto-generates +1 Dread/sec", effect: 1 },
harvester: { level: 0, cost: 200, costScale: 1.7, name: "Soul Harvester", desc: "Auto-generates +0.5 Souls/sec", effect: 0.5 },
incubator: { level: 0, cost: 500, costScale: 1.8, name: "Corrupt Incubator", desc: "Monsters hatch 1 tier higher base", effect: 1 },
amplifier: { level: 0, cost: 1000, costScale: 1.9, name: "Dread Amplifier", desc: "All monster production ×1.5", effect: 1.5 },
voidgate: { level: 0, cost: 5000, costScale: 2.0, name: "Void Gate", desc: "Unlocks Realm farming", effect: 1 },
mutagen: { level: 0, cost: 10000, costScale: 2.2, name: "Mutagen Vat", desc: "Evolution cost reduced by 10%", effect: 0.9 },
},
zones: {
crypt: { unlocked: true, name: "Forgotten Crypt", desc: "Ancient burial grounds teeming with weak spirits", dreadRate: 5, soulRate: 0, danger: 0.05, reward: "+5 Dread/sec" },
swamp: { unlocked: false, name: "Rotting Swamp", desc: "Bubbling miasma corrupts everything it touches", dreadRate: 15, soulRate: 1, danger: 0.1, reward: "+15 Dread, +1 Soul/sec" },
abyss: { unlocked: false, name: "The Abyss", desc: "Stare into the void. It stares back.", dreadRate: 50, soulRate: 5, danger: 0.2, reward: "+50 Dread, +5 Souls/sec" },
nullspace: { unlocked: false, name: "Null Space", desc: "Reality unravels. Only the strongest survive.", dreadRate: 200, soulRate: 20, danger: 0.35, reward: "+200 Dread, +20 Souls/sec" },
},
activeZone: null,
zoneProgress: 0,
zoneHealth: 100,
};

// ==================== MONSTER DATA ====================
const MONSTER_TYPES = [
// Tier 0 - Larvae
{ tier: 0, name: "Grubspawn", hp: 10, atk: 1, def: 0, spd: 1, prod: 0.5, evo: "Mawling", palette: [[0,80,0],[40,120,40],[20,60,20]] },
{ tier: 0, name: "Boneling", hp: 8, atk: 2, def: 0, spd: 2, prod: 0.4, evo: "Ribcage", palette: [[200,200,200],[150,150,150],[100,100,100]] },
{ tier: 0, name: "Sludge", hp: 15, atk: 1, def: 1, spd: 0, prod: 0.6, evo: "Muckfiend", palette: [[60,40,20],[100,80,40],[80,60,30]] },
// Tier 1 - Juveniles
{ tier: 1, name: "Mawling", hp: 30, atk: 4, def: 1, spd: 3, prod: 1.5, evo: "Goremaw", palette: [[120,0,0],[180,20,20],[80,0,0]] },
{ tier: 1, name: "Ribcage", hp: 25, atk: 5, def: 2, spd: 4, prod: 1.2, evo: "Ossuary", palette: [[220,220,220],[180,180,180],[120,120,120]] },
{ tier: 1, name: "Muckfiend", hp: 40, atk: 3, def: 3, spd: 1, prod: 1.8, evo: "Rotlord", palette: [[80,60,30],[120,100,50],[60,40,10]] },
{ tier: 1, name: "Shadelet", hp: 20, atk: 6, def: 0, spd: 5, prod: 1.0, evo: "Dreadwraith", palette: [[40,0,60],[80,20,100],[20,0,40]] },
// Tier 2 - Adults
{ tier: 2, name: "Goremaw", hp: 80, atk: 10, def: 3, spd: 5, prod: 4.0, evo: "Fleshbeast", palette: [[160,0,0],[220,40,40],[100,0,0]] },
{ tier: 2, name: "Ossuary", hp: 70, atk: 12, def: 5, spd: 6, prod: 3.5, evo: "Catacomb", palette: [[240,240,240],[200,200,200],[140,140,140]] },
{ tier: 2, name: "Rotlord", hp: 100, atk: 8, def: 6, spd: 2, prod: 5.0, evo: "Plaguefather", palette: [[100,80,40],[140,120,60],[80,60,20]] },
{ tier: 2, name: "Dreadwraith", hp: 60, atk: 15, def: 1, spd: 10, prod: 3.0, evo: "Voidreaper", palette: [[60,0,80],[100,30,120],[40,0,60]] },
// Tier 3 - Elders
{ tier: 3, name: "Fleshbeast", hp: 200, atk: 25, def: 8, spd: 6, prod: 12.0, evo: "Abomination", palette: [[180,20,20],[240,60,60],[120,0,0]] },
{ tier: 3, name: "Catacomb", hp: 180, atk: 28, def: 12, spd: 8, prod: 10.0, evo: "Gravemind", palette: [[255,255,255],[220,220,220],[160,160,160]] },
{ tier: 3, name: "Plaguefather", hp: 250, atk: 20, def: 15, spd: 3, prod: 15.0, evo: "Pestilence", palette: [[120,100,50],[160,140,70],[100,80,30]] },
{ tier: 3, name: "Voidreaper", hp: 150, atk: 35, def: 4, spd: 15, prod: 9.0, evo: "Null Emperor", palette: [[80,0,100],[120,40,140],[60,0,80]] },
// Tier 4 - Apex
{ tier: 4, name: "Abomination", hp: 600, atk: 60, def: 20, spd: 8, prod: 40.0, evo: null, palette: [[200,0,0],[255,50,50],[150,0,0]] },
{ tier: 4, name: "Gravemind", hp: 550, atk: 65, def: 25, spd: 10, prod: 35.0, evo: null, palette: [[255,255,255],[230,230,230],[180,180,180]] },
{ tier: 4, name: "Pestilence", hp: 700, atk: 50, def: 30, spd: 4, prod: 50.0, evo: null, palette: [[140,120,60],[180,160,80],[120,100,40]] },
{ tier: 4, name: "Null Emperor", hp: 500, atk: 80, def: 10, spd: 20, prod: 30.0, evo: null, palette: [[100,0,120],[140,50,160],[80,0,100]] },
];

const HATCH_COST_BASE = 10;
const HATCH_COST_SCALE = 1.15;
let hatchCount = 0;

// ==================== PIXEL ART GENERATOR ====================
function generateMonsterSprite(type, size = 64) {
const canvas = document.createElement('canvas');
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext('2d');
const seed = type.name.split('').reduce((a,b)=>a+b.charCodeAt(0),0);
const rng = mulberry32(seed + Date.now() % 10000);

const palette = type.palette;
const px = size / 16;

// Background glow
ctx.fillStyle = `rgba(${palette[0][0]},${palette[0][1]},${palette[0][2]},0.1)`;
ctx.fillRect(0,0,size,size);

// Body shape based on tier
const bodyColor = `rgb(${palette[0][0]},${palette[0][1]},${palette[0][2]})`;
const detailColor = `rgb(${palette[1][0]},${palette[1][1]},${palette[1][2]})`;
const darkColor = `rgb(${palette[2][0]},${palette[2][1]},${palette[2][2]})`;

ctx.fillStyle = bodyColor;

// Procedural body
const cx = 8, cy = 8;
const bodyW = 6 + type.tier + Math.floor(rng()*4);
const bodyH = 6 + type.tier + Math.floor(rng()*4);

for(let y=0; y<16; y++) {
for(let x=0; x<16; x++) {
const dx = x - cx;
const dy = y - cy;
const dist = Math.sqrt(dx*dx + dy*dy);
const noise = rng();

if (dist < bodyW/2 + noise) {
// Core body
ctx.fillStyle = bodyColor;
ctx.fillRect(x*px, y*px, px, px);
} else if (dist < bodyW/2 + 2 && noise > 0.4) {
// Details / limbs
ctx.fillStyle = detailColor;
ctx.fillRect(x*px, y*px, px, px);
} else if (dist < bodyW/2 + 3 && noise > 0.7) {
// Dark accents
ctx.fillStyle = darkColor;
ctx.fillRect(x*px, y*px, px, px);
}
}
}

// Eyes
const eyeY = cy - 1 + Math.floor(rng()*3);
const eyeX1 = cx - 2 - Math.floor(rng()*2);
const eyeX2 = cx + 2 + Math.floor(rng()*2);
ctx.fillStyle = '#ff0000';
if (rng() > 0.3) ctx.fillRect(eyeX1*px, eyeY*px, px, px);
if (rng() > 0.3) ctx.fillRect(eyeX2*px, eyeY*px, px, px);

// Mouth
if (type.tier >= 1) {
ctx.fillStyle = darkColor;
for(let i=-2; i<=2; i++) {
if (rng() > 0.2) ctx.fillRect((cx+i)*px, (cy+3)*px, px, px);
}
}

// Horns / spikes for higher tiers
if (type.tier >= 2) {
ctx.fillStyle = detailColor;
ctx.fillRect((cx-3)*px, (cy-4)*px, px, px*2);
ctx.fillRect((cx+3)*px, (cy-4)*px, px, px*2);
}
if (type.tier >= 3) {
ctx.fillStyle = '#fff';
ctx.fillRect((cx-1)*px, (cy-5)*px, px, px*2);
ctx.fillRect((cx+1)*px, (cy-5)*px, px, px*2);
}

return canvas;
}

function mulberry32(a) {
return function() {
let t = a += 0x6D2B79F5;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
}

// ==================== CORE FUNCTIONS ====================
function getHatchCost() {
return Math.floor(HATCH_COST_BASE * Math.pow(HATCH_COST_SCALE, hatchCount));
}

function hatchMonster() {
const cost = getHatchCost();
if (state.dread < cost) {
showToast("Not enough Dread!");
return;
}

state.dread -= cost;
hatchCount++;

const bonus = state.upgrades.incubator.level;
const available = MONSTER_TYPES.filter(m => m.tier <= Math.min(bonus, 4));
const type = available[Math.floor(Math.random() * available.length)];

const monster = {
id: state.nextId++,
type: type,
level: 1,
xp: 0,
xpToLevel: 10,
hp: type.hp,
maxHp: type.hp,
atk: type.atk,
def: type.def,
spd: type.spd,
prod: type.prod,
sprite: null,
};

monster.sprite = generateMonsterSprite(type);
state.monsters.push(monster);

log(`Hatched ${type.name}!`);
showToast(`Hatched: ${type.name}`);
spawnParticles(window.innerWidth/2, window.innerHeight/2, type.palette[0]);
render();
}

function levelUpMonster(m) {
const cost = Math.floor(m.xpToLevel * 0.5);
if (state.dread < cost) {
showToast("Not enough Dread!");
return;
}
state.dread -= cost;
m.level++;
m.xp = 0;
m.xpToLevel = Math.floor(m.xpToLevel * 1.5);
m.maxHp = Math.floor(m.maxHp * 1.2);
m.hp = m.maxHp;
m.atk = Math.floor(m.atk * 1.15);
m.def = Math.floor(m.def * 1.1);
m.spd = Math.floor(m.spd * 1.1);
m.prod = m.prod * 1.1;
log(`${m.type.name} reached level ${m.level}!`);
showToast(`${m.type.name} Lv.${m.level}`);
render();
}

function evolveMonster(m) {
if (!m.type.evo) {
showToast("Cannot evolve further!");
return;
}
const evoName = m.type.evo;
const evoType = MONSTER_TYPES.find(t => t.name === evoName);
if (!evoType) return;

let cost = Math.floor(100 * Math.pow(2, evoType.tier) * Math.pow(state.upgrades.mutagen ? 0.9 : 1, state.upgrades.mutagen.level));
if (state.souls < cost) {
showToast(`Need ${cost} Souls!`);
return;
}

state.souls -= cost;
m.type = evoType;
m.level = 1;
m.xp = 0;
m.xpToLevel = 20;
m.maxHp = evoType.hp;
m.hp = evoType.hp;
m.atk = evoType.atk;
m.def = evoType.def;
m.spd = evoType.spd;
m.prod = evoType.prod;
m.sprite = generateMonsterSprite(evoType);

log(`${m.type.name} has EVOLVED!`);
showToast(`EVOLVED: ${evoType.name}!`);
spawnParticles(window.innerWidth/2, window.innerHeight/2, evoType.palette[0]);
closeModal();
render();
}

function sacrificeMonster(m) {
const soulGain = Math.floor(m.level * m.type.tier * 5 + 1);
state.souls += soulGain;
state.monsters = state.monsters.filter(x => x.id !== m.id);
log(`Sacrificed ${m.type.name} for ${soulGain} Souls.`);
showToast(`+${soulGain} Souls`);
closeModal();
render();
}

function buyUpgrade(key) {
const up = state.upgrades[key];
if (state.dread < up.cost) {
showToast("Not enough Dread!");
return;
}
state.dread -= up.cost;
up.level++;
up.cost = Math.floor(up.cost * up.costScale);

if (key === 'voidgate') {
state.zones.swamp.unlocked = true;
}

log(`Purchased: ${up.name} Lv.${up.level}`);
showToast(`${up.name} +1`);
render();
}

function enterZone(key) {
const zone = state.zones[key];
if (!zone.unlocked) {
showToast("Zone locked!");
return;
}
if (state.monsters.length === 0) {
showToast("You need monsters to farm!");
return;
}
state.activeZone = key;
state.zoneProgress = 0;
state.zoneHealth = 100;
log(`Entered ${zone.name}...`);
showToast(`Farming: ${zone.name}`);
render();
}

// ==================== PRODUCTION ====================
function gameTick() {
// Monster production
let mult = Math.pow(state.upgrades.amplifier.effect, state.upgrades.amplifier.level);
if (mult === Infinity || isNaN(mult)) mult = 1;

for (const m of state.monsters) {
const gain = m.prod * m.level * mult / 10; // per tick (100ms)
state.dread += gain;
state.totalDread += gain;
m.xp += 0.1 * m.level;
if (m.xp >= m.xpToLevel) {
m.xp = m.xpToLevel; // cap, manual level up
}
}

// Automation
if (state.upgrades.extractor.level > 0) {
state.dread += state.upgrades.extractor.level * state.upgrades.extractor.effect / 10;
state.totalDread += state.upgrades.extractor.level * state.upgrades.extractor.effect / 10;
}
if (state.upgrades.harvester.level > 0) {
state.souls += state.upgrades.harvester.level * state.upgrades.harvester.effect / 10;
}

// Zone farming
if (state.activeZone) {
const zone = state.zones[state.activeZone];
state.zoneProgress += 1;

// Danger check every 10 ticks
if (state.zoneProgress % 10 === 0) {
if (Math.random() < zone.danger) {
// Damage random monster
const target = state.monsters[Math.floor(Math.random() * state.monsters.length)];
const dmg = Math.floor(Math.random() * 10) + 5;
target.hp -= dmg;
if (target.hp <= 0) {
log(`${target.type.name} was consumed by the ${zone.name}...`);
state.monsters = state.monsters.filter(m => m.id !== target.id);
if (state.monsters.length === 0) {
state.activeZone = null;
log("All monsters lost. Retreating...");
}
}
}
}

// Rewards
state.dread += zone.dreadRate / 10;
state.totalDread += zone.dreadRate / 10;
state.souls += zone.soulRate / 10;

// Zone unlocks
if (state.activeZone === 'swamp' && state.zoneProgress > 300) {
state.zones.abyss.unlocked = true;
}
if (state.activeZone === 'abyss' && state.zoneProgress > 600) {
state.zones.nullspace.unlocked = true;
}
}

updateResources();
if (state.tab === 'monsters') renderMonsters();
if (state.tab === 'zones') renderZones();
}

function updateResources() {
document.getElementById('resDread').textContent = formatNum(state.dread);
document.getElementById('resSouls').textContent = formatNum(state.souls);
document.getElementById('resShards').textContent = formatNum(state.shards);
}

function formatNum(n) {
if (n >= 1e12) return (n/1e12).toFixed(2) + 'T';
if (n >= 1e9) return (n/1e9).toFixed(2) + 'B';
if (n >= 1e6) return (n/1e6).toFixed(2) + 'M';
if (n >= 1e3) return (n/1e3).toFixed(2) + 'K';
return Math.floor(n).toString();
}

// ==================== RENDERING ====================
function render() {
const content = document.getElementById('content');
content.innerHTML = '';

swit