Created
July 29, 2026 20:59
-
-
Save qtqgyt/c0a6f33d64e10d160d2e67efc5a82940 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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">▶</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