Skip to content

Instantly share code, notes, and snippets.

@qtqgyt
Created July 29, 2026 20:59
Show Gist options
  • Select an option

  • Save qtqgyt/c0a6f33d64e10d160d2e67efc5a82940 to your computer and use it in GitHub Desktop.

Select an option

Save qtqgyt/c0a6f33d64e10d160d2e67efc5a82940 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scrap Mechanic Raid Calculator</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #e0e0e0; padding: 20px; }
.container { max-width: 900px; margin: 0 auto; }
h1 { color: #ff6b35; font-size: 1.8rem; margin-bottom: 8px; }
.subtitle { color: #888; margin-bottom: 24px; font-size: 0.9rem; }
.card { background: #16213e; border-radius: 8px; padding: 20px; margin-bottom: 16px; border: 1px solid #0f3460; }
.card h2 { color: #e94560; font-size: 1.1rem; margin-bottom: 12px; }
.row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.crop-input { display: flex; align-items: center; gap: 6px; background: #1a1a2e; padding: 6px 10px; border-radius: 4px; border: 1px solid #0f3460; width: calc(50% - 4px); }
@media (min-width: 700px) { .crop-input { width: calc(25% - 6px); } }
.crop-input label { flex: 1; font-size: 0.85rem; white-space: nowrap; }
.crop-input .val { color: #888; font-size: 0.75rem; margin-right: 4px; }
.crop-input input { width: 52px; padding: 4px 6px; border: 1px solid #333; border-radius: 3px; background: #0f3460; color: #e0e0e0; text-align: center; font-size: 0.85rem; }
.crop-input input:focus { outline: none; border-color: #e94560; }
.players-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.players-row label { font-size: 0.9rem; }
.players-row input { width: 60px; padding: 4px 8px; border: 1px solid #333; border-radius: 3px; background: #0f3460; color: #e0e0e0; text-align: center; font-size: 0.9rem; }
.results-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 16px; }
.stat { text-align: center; padding: 12px; background: #0f3460; border-radius: 6px; }
.stat .label { font-size: 0.75rem; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
.stat .value { font-size: 1.6rem; font-weight: bold; margin-top: 4px; }
.stat .value.level-0 { color: #666; }
.stat .value.level-1 { color: #4caf50; }
.stat .value.level-2 { color: #8bc34a; }
.stat .value.level-3 { color: #ffeb3b; }
.stat .value.level-4 { color: #ff9800; }
.stat .value.level-5 { color: #ff5722; }
.stat .value.level-6 { color: #f44336; }
.stat .value.level-7 { color: #9c27b0; }
.stat .value.super { color: #9c27b0; }
.progress-bar { height: 8px; background: #0f3460; border-radius: 4px; margin: 8px 0; overflow: hidden; }
.progress-bar .fill { height: 100%; width: 0%; transition: width 0.3s; }
.progress-bar .fill.level-1 { background: #4caf50; }
.progress-bar .fill.level-2 { background: #8bc34a; }
.progress-bar .fill.level-3 { background: #ffeb3b; }
.progress-bar .fill.level-4 { background: #ff9800; }
.progress-bar .fill.level-5 { background: #ff5722; }
.progress-bar .fill.level-6 { background: #f44336; }
.progress-bar .fill.level-7 { background: #9c27b0; }
.spawn-section { margin-top: 12px; }
.spawn-group { background: #0f3460; border-radius: 4px; padding: 8px 12px; margin-bottom: 6px; font-size: 0.85rem; display: flex; justify-content: space-between; align-items: center; }
.spawn-group .enemies { display: flex; gap: 6px; flex-wrap: wrap; }
.spawn-group .enemy-tag { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 0.78rem; }
.enemy-haybot { background: #5d4037; color: #d7ccc8; }
.enemy-totebot-green { background: #2e7d32; color: #c8e6c9; }
.enemy-totebot-blue { background: #1565c0; color: #bbdefb; }
.enemy-totebot-red { background: #c62828; color: #ffcdd2; }
.enemy-totebot-yellow { background: #f9a825; color: #332; }
.enemy-tapebot-green { background: #00695c; color: #b2dfdb; }
.enemy-tapebot-yellow { background: #e65100; color: #ffe0b2; }
.enemy-tapebot { background: #4a148c; color: #e1bee7; }
.enemy-farmbot { background: #b71c1c; color: #ffcdd2; }
.spawn-group .cost { color: #888; font-size: 0.75rem; }
.spawn-group .weight-bar { height: 3px; background: #333; border-radius: 2px; margin-top: 4px; }
.spawn-group .weight-bar .fill { height: 100%; background: #e94560; border-radius: 2px; }
.initial-label { color: #ff6b35; font-weight: bold; }
.delay-info { color: #666; font-size: 0.75rem; }
.toggle-group { margin-top: 8px; }
.toggle-header { cursor: pointer; color: #e94560; font-size: 0.85rem; user-select: none; display: flex; align-items: center; gap: 6px; }
.toggle-header:hover { color: #ff6b81; }
.toggle-content { margin-top: 8px; }
.hidden { display: none; }
</style>
</head>
<body>
<div class="container">
<h1>Raid Calculator</h1>
<p class="subtitle">Scrap Mechanic Survival — predicts raid level, budget, and enemies based on planted crops</p>
<div class="card">
<h2>Crops Planted</h2>
<div class="row" id="cropInputs"></div>
<div class="players-row">
<label>Players:</label>
<input type="number" id="playerCount" value="1" min="1" max="99">
<span style="color:#666;font-size:0.85rem;">Multiplier: <span id="mpMult">1.0</span>x</span>
</div>
</div>
<div class="card" id="resultsCard">
<h2>Raid Prediction</h2>
<div class="results-grid">
<div class="stat"><div class="label">Total Value</div><div class="value" id="totalValue">0</div></div>
<div class="stat"><div class="label">Raid Level</div><div class="value level-0" id="raidLevel">-</div></div>
<div class="stat"><div class="label">Budget</div><div class="value" id="raidBudget">0</div></div>
<div class="stat"><div class="label">Enemies</div><div class="value" id="enemyCount">0</div></div>
</div>
<div class="progress-bar"><div class="fill" id="levelProgress"></div></div>
<div style="display:flex;justify-content:space-between;font-size:0.75rem;color:#666;">
<span id="levelLabel">Level 1</span>
<span id="nextLevelLabel">Level 2 (50)</span>
</div>
<div class="spawn-section">
<div style="font-size:0.85rem;margin-bottom:8px;">Spawn Timeline</div>
<div id="spawnTimeline"></div>
</div>
<div class="toggle-group">
<div class="toggle-header" onclick="toggleBudget()">
<span id="budgetArrow">&#9654;</span> Budget Spending Details
</div>
<div class="toggle-content hidden" id="budgetDetails"></div>
</div>
</div>
</div>
<script>
const CROP_VALUES = {
tomato: 1, potato: 1, carrot: 2, redbeet: 5,
banana: 15, chili: 15, blueberry: 50, orange: 100,
broccoli: 500, pineapple: 1000
};
const LEVEL_THRESHOLDS = [0, 50, 100, 550, 1000, 5500, 10000];
const RAID_MAX_VALUE = 100000;
const LEVEL_BUDGETS = {
1: [2, 30], 2: [20, 50], 3: [75, 135],
4: [125, 200], 5: [300, 500], 6: [500, 700], 7: [1000, 5000]
};
const INITIAL_SPAWNS = {
1: [{ uuid: 'haybot', qty: 1 }, { uuid: 'totebot_green', qty: 2 }],
2: [{ uuid: 'haybot', qty: 3 }, { uuid: 'totebot_green', qty: 2 }],
3: [{ uuid: 'haybot', qty: 3 }, { uuid: 'totebot_green', qty: 2 }, { uuid: 'totebot_blue', qty: 1 }],
4: [{ uuid: 'haybot', qty: 2 }, { uuid: 'totebot_blue', qty: 1 }, { uuid: 'tapebot_green', qty: 1 }, { uuid: 'tapebot_yellow', qty: 1 }],
5: [{ uuid: 'farmbot', qty: 1 }],
6: [{ uuid: 'farmbot', qty: 1 }],
7: [{ uuid: 'farmbot', qty: 3 }]
};
const LEVEL_GROUPS = {
1: [
{ enemies: [{ uuid: 'totebot_green', qty: 1 }], cost: 2, weight: 1 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }], cost: 4, weight: 10 },
{ enemies: [{ uuid: 'haybot', qty: 1 }], cost: 5, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'haybot', qty: 1 }], cost: 9, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 2 }], cost: 12, weight: 100 },
],
2: [
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 1 }], cost: 7, weight: 1 },
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 1 }, { uuid: 'totebot_blue', qty: 1 }], cost: 12, weight: 10 },
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 2 }, { uuid: 'totebot_blue', qty: 1 }], cost: 17, weight: 50 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'haybot', qty: 3 }], cost: 19, weight: 100 },
],
3: [
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 1 }], cost: 7, weight: 1 },
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 1 }, { uuid: 'totebot_blue', qty: 1 }], cost: 12, weight: 10 },
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 2 }, { uuid: 'totebot_blue', qty: 1 }], cost: 17, weight: 50 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'haybot', qty: 3 }], cost: 19, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'totebot_red', qty: 1 }], cost: 24, weight: 50 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'tapebot_green', qty: 1 }, { uuid: 'tapebot_yellow', qty: 1 }], cost: 16, weight: 50 },
],
4: [
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 1 }], cost: 7, weight: 1 },
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 1 }, { uuid: 'totebot_blue', qty: 1 }], cost: 12, weight: 10 },
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 2 }, { uuid: 'totebot_blue', qty: 1 }], cost: 17, weight: 50 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'haybot', qty: 3 }], cost: 19, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'totebot_red', qty: 1 }], cost: 19, weight: 50 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'totebot_yellow', qty: 1 }], cost: 19, weight: 50 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'tapebot_green', qty: 1 }, { uuid: 'tapebot_yellow', qty: 1 }], cost: 16, weight: 50 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'tapebot_green', qty: 2 }, { uuid: 'tapebot_yellow', qty: 1 }], cost: 19, weight: 50 },
],
5: [
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 1 }], cost: 7, weight: 1 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'haybot', qty: 2 }], cost: 14, weight: 10 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'haybot', qty: 3 }], cost: 21, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'totebot_blue', qty: 3 }], cost: 21, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'totebot_yellow', qty: 2 }], cost: 36, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'totebot_red', qty: 2 }], cost: 36, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'tapebot_green', qty: 3 }, { uuid: 'tapebot_yellow', qty: 2 }], cost: 31, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'tapebot', qty: 2 }], cost: 56, weight: 80 },
{ enemies: [{ uuid: 'farmbot', qty: 1 }], cost: 75, weight: 60 },
],
6: [
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 1 }], cost: 7, weight: 1 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'haybot', qty: 2 }], cost: 14, weight: 10 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'haybot', qty: 3 }], cost: 21, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'totebot_blue', qty: 3 }], cost: 21, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'totebot_yellow', qty: 2 }], cost: 36, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'totebot_red', qty: 2 }], cost: 36, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'tapebot_green', qty: 3 }, { uuid: 'tapebot_yellow', qty: 2 }], cost: 31, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'tapebot', qty: 2 }], cost: 56, weight: 80 },
{ enemies: [{ uuid: 'farmbot', qty: 1 }], cost: 75, weight: 60 },
],
7: [
{ enemies: [{ uuid: 'totebot_green', qty: 1 }, { uuid: 'haybot', qty: 1 }], cost: 7, weight: 1 },
{ enemies: [{ uuid: 'totebot_green', qty: 2 }, { uuid: 'haybot', qty: 2 }], cost: 14, weight: 10 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'haybot', qty: 3 }], cost: 21, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'totebot_blue', qty: 3 }], cost: 21, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'totebot_yellow', qty: 2 }], cost: 36, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'totebot_red', qty: 2 }], cost: 36, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'tapebot_green', qty: 3 }, { uuid: 'tapebot_yellow', qty: 2 }], cost: 31, weight: 100 },
{ enemies: [{ uuid: 'totebot_green', qty: 3 }, { uuid: 'tapebot', qty: 2 }], cost: 56, weight: 80 },
{ enemies: [{ uuid: 'farmbot', qty: 1 }], cost: 75, weight: 60 },
],
};
const ENEMY_LABELS = {
haybot: 'Haybot',
totebot_green: 'Green Totebot',
totebot_blue: 'Blue Totebot',
totebot_red: 'Red Totebot',
totebot_yellow: 'Yellow Totebot',
tapebot_green: 'Green Tapebot',
tapebot_yellow: 'Yellow Tapebot',
tapebot: 'Tapebot',
farmbot: 'Farmbot',
};
const ENEMY_CLASSES = {
haybot: 'enemy-haybot',
totebot_green: 'enemy-totebot-green',
totebot_blue: 'enemy-totebot-blue',
totebot_red: 'enemy-totebot-red',
totebot_yellow: 'enemy-totebot-yellow',
tapebot_green: 'enemy-tapebot-green',
tapebot_yellow: 'enemy-tapebot-yellow',
tapebot: 'enemy-tapebot',
farmbot: 'enemy-farmbot',
};
const CROP_DISPLAY = [
{ id: 'tomato', label: 'Tomato', emoji: '' },
{ id: 'potato', label: 'Potato', emoji: '' },
{ id: 'carrot', label: 'Carrot', emoji: '' },
{ id: 'redbeet', label: 'Red Beet', emoji: '' },
{ id: 'banana', label: 'Banana', emoji: '' },
{ id: 'chili', label: 'Chili', emoji: '' },
{ id: 'blueberry', label: 'Blueberry', emoji: '' },
{ id: 'orange', label: 'Orange', emoji: '' },
{ id: 'broccoli', label: 'Broccoli', emoji: '' },
{ id: 'pineapple', label: 'Pineapple', emoji: '' },
];
const LEVEL_NAMES = ['0', '1', '2', '3', '4', '5', '6', '7'];
function getRaidLevel(value) {
if (value > LEVEL_THRESHOLDS[LEVEL_THRESHOLDS.length - 1]) return 7;
for (let i = LEVEL_THRESHOLDS.length - 1; i >= 0; i--) {
if (value >= LEVEL_THRESHOLDS[i]) return Math.min(i, LEVEL_THRESHOLDS.length - 2) + 1;
}
return 1;
}
function getLevelFraction(value) {
if (value <= 0) return 0;
if (value >= RAID_MAX_VALUE) return 1;
const level = getRaidLevel(value);
const lo = LEVEL_THRESHOLDS[level - 1] || 0;
const hi = level < LEVEL_THRESHOLDS.length ? LEVEL_THRESHOLDS[level] : RAID_MAX_VALUE;
return Math.max(0, Math.min(1, (value - lo) / (hi - lo)));
}
function getBudget(value, players) {
const level = getRaidLevel(value);
const fraction = getLevelFraction(value);
const mp = Math.min(1 + (players - 1) * 0.5, 2);
const [minB, maxB] = LEVEL_BUDGETS[level];
return Math.ceil(fraction * (maxB - minB) + minB * mp);
}
function simulateBudgetSpend(budget, level) {
const groups = LEVEL_GROUPS[level];
const selected = [];
let remaining = budget;
while (remaining > 0) {
const affordable = groups.filter(g => g.cost <= remaining);
if (affordable.length === 0) break;
let minCost = Infinity;
for (const g of affordable) if (g.cost < minCost) minCost = g.cost;
const totalWeight = affordable.reduce((s, g) => s + g.weight, 0);
let roll = Math.random() * totalWeight;
let chosen = affordable[0];
for (const g of affordable) {
roll -= g.weight;
if (roll <= 0) { chosen = g; break; }
}
selected.push(chosen);
remaining -= chosen.cost;
}
return selected;
}
function getExpectedSpawns(budget, level) {
const groups = LEVEL_GROUPS[level];
const trials = 10000;
let totalCounts = {};
for (const g of groups) {
const key = JSON.stringify(g.enemies.map(e => ({ uuid: e.uuid, qty: e.qty })));
totalCounts[key] = 0;
}
for (let t = 0; t < trials; t++) {
let remaining = budget;
while (remaining > 0) {
const affordable = groups.filter(g => g.cost <= remaining);
if (affordable.length === 0) break;
const totalWeight = affordable.reduce((s, g) => s + g.weight, 0);
let roll = Math.random() * totalWeight;
let chosen = affordable[0];
for (const g of affordable) {
roll -= g.weight;
if (roll <= 0) { chosen = g; break; }
}
const key = JSON.stringify(chosen.enemies.map(e => ({ uuid: e.uuid, qty: e.qty })));
totalCounts[key]++;
remaining -= chosen.cost;
}
}
const result = [];
for (const g of groups) {
const key = JSON.stringify(g.enemies.map(e => ({ uuid: e.uuid, qty: e.qty })));
const count = totalCounts[key] || 0;
if (count > 0) {
result.push({
enemies: g.enemies,
cost: g.cost,
weight: g.weight,
expectedCount: count / trials,
expectedBudget: (count / trials) * g.cost,
});
}
}
result.sort((a, b) => b.expectedCount - a.expectedCount);
return result;
}
function enemyTagHtml(uuid, qty) {
const label = ENEMY_LABELS[uuid] || uuid;
const cls = ENEMY_CLASSES[uuid] || '';
const plural = qty > 1 ? label + 's' : label;
return `<span class="enemy-tag ${cls}">${qty}x ${plural}</span>`;
}
function enemyGroupHtml(enemies, extra) {
const tags = enemies.map(e => enemyTagHtml(e.uuid, e.qty)).join('');
return `<div class="enemies">${tags}</div>`;
}
function render() {
let totalValue = 0;
for (const crop of CROP_DISPLAY) {
const el = document.getElementById('crop_' + crop.id);
const val = parseInt(el.value) || 0;
totalValue += val * CROP_VALUES[crop.id];
}
const players = parseInt(document.getElementById('playerCount').value) || 1;
const mp = Math.min(1 + (players - 1) * 0.5, 2);
document.getElementById('mpMult').textContent = mp.toFixed(1);
const level = totalValue > 0 ? getRaidLevel(totalValue) : 0;
const fraction = totalValue > 0 ? getLevelFraction(totalValue) : 0;
const budget = totalValue > 0 ? getBudget(totalValue, players) : 0;
document.getElementById('totalValue').textContent = totalValue;
const levelEl = document.getElementById('raidLevel');
levelEl.textContent = level === 0 ? '-' : level === 7 ? '7 (Super)' : level;
levelEl.className = 'value level-' + level;
document.getElementById('raidBudget').textContent = budget;
const progress = document.getElementById('levelProgress');
const levelLabel = document.getElementById('levelLabel');
const nextLabel = document.getElementById('nextLevelLabel');
if (level > 0) {
const pct = level === 7 ? Math.min(fraction * 100, 100) : ((level - 1) * (100 / 7) + fraction * (100 / 7));
progress.style.width = Math.min(pct, 100) + '%';
progress.className = 'fill level-' + level;
if (level < 7) {
levelLabel.textContent = 'Level ' + level + ' (' + (LEVEL_THRESHOLDS[level - 1] || 0) + '–' + ((LEVEL_THRESHOLDS[level] || RAID_MAX_VALUE) - 1) + ')';
} else {
levelLabel.textContent = 'Level 7 Super (10000+)';
}
if (level < 7) {
nextLabel.textContent = 'Level ' + (level + 1) + ' (' + LEVEL_THRESHOLDS[level] + '+)';
} else {
nextLabel.textContent = 'Max level reached';
}
} else {
progress.style.width = '0%';
levelLabel.textContent = '—';
nextLabel.textContent = '—';
}
// Enemy count
let totalEnemies = 0;
if (level > 0) {
const initial = INITIAL_SPAWNS[level];
for (const e of initial) totalEnemies += e.qty;
const expected = getExpectedSpawns(budget, level);
for (const g of expected) {
for (const e of g.enemies) totalEnemies += e.qty * g.expectedCount;
}
}
document.getElementById('enemyCount').textContent = Math.round(totalEnemies);
// Timeline
const timeline = document.getElementById('spawnTimeline');
timeline.innerHTML = '';
if (level === 0) {
timeline.innerHTML = '<div style="color:#666;font-size:0.85rem;">Plant crops to see raid prediction</div>';
return;
}
let tick = 0;
const addGroup = (label, enemies, delaySec) => {
const div = document.createElement('div');
div.className = 'spawn-group';
const sec = (tick / 40).toFixed(1);
div.innerHTML = `<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<span style="color:#888;font-size:0.75rem;min-width:60px;">t+${sec}s</span>
${label}
${enemyGroupHtml(enemies)}
</div>`;
timeline.appendChild(div);
tick += delaySec * 40;
};
// Initial
const initial = INITIAL_SPAWNS[level];
const initDelay = 12;
addGroup('<span class="initial-label">Initial</span>', initial, initDelay);
// Budget groups
const expected = getExpectedSpawns(budget, level);
const budgetDelay = 2;
for (const g of expected) {
const roundedCount = Math.round(g.expectedCount * 10) / 10;
if (roundedCount < 0.05) continue;
const count = Math.round(roundedCount);
for (let i = 0; i < Math.max(1, count); i++) {
const label = `<span style="color:#888;font-size:0.75rem;">Budget</span>`;
addGroup(label, g.enemies, budgetDelay);
}
}
// Budget details
const details = document.getElementById('budgetDetails');
details.innerHTML = '<div style="font-size:0.85rem;margin-bottom:6px;">Expected groups bought with budget of ' + budget + ' (simulated 10,000 runs):</div>';
if (expected.length === 0) {
details.innerHTML += '<div style="color:#666;font-size:0.85rem;">No budget groups possible</div>';
} else {
for (const g of expected) {
const pct = (g.expectedCount / Math.ceil(budget / g.cost) * 100).toFixed(0);
const div = document.createElement('div');
div.className = 'spawn-group';
const unitStr = g.enemies.map(e => `${e.qty}x ${ENEMY_LABELS[e.uuid]}`).join(', ');
div.innerHTML = `
<div style="flex:1;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span>${enemyGroupHtml(g.enemies)}</span>
<span class="cost">cost ${g.cost} / weight ${g.weight}</span>
</div>
<div style="font-size:0.75rem;color:#666;margin-top:2px;">
~${(g.expectedCount).toFixed(2)}x per raid (${(g.expectedCount * g.cost / budget * 100).toFixed(0)}% of budget)
</div>
<div class="weight-bar"><div class="fill" style="width:${(g.weight / 100 * 100).toFixed(0)}%"></div></div>
</div>`;
details.appendChild(div);
}
}
}
function buildInputs() {
const container = document.getElementById('cropInputs');
container.innerHTML = '';
for (const crop of CROP_DISPLAY) {
const div = document.createElement('div');
div.className = 'crop-input';
div.innerHTML = `
<label for="crop_${crop.id}">${crop.label}</label>
<span class="val">${CROP_VALUES[crop.id]}</span>
<input type="number" id="crop_${crop.id}" value="0" min="0" max="9999">
`;
container.appendChild(div);
const input = div.querySelector('input');
input.addEventListener('input', render);
}
document.getElementById('playerCount').addEventListener('input', render);
}
function toggleBudget() {
const content = document.getElementById('budgetDetails');
const arrow = document.getElementById('budgetArrow');
content.classList.toggle('hidden');
arrow.textContent = content.classList.contains('hidden') ? '\u25B6' : '\u25BC';
if (!content.classList.contains('hidden')) render();
}
buildInputs();
render();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment