cards and lot of
This commit is contained in:
parent
6d6fbe85a9
commit
ca40a0cb73
13 changed files with 624 additions and 92 deletions
61
README.md
61
README.md
|
|
@ -1,25 +1,40 @@
|
|||
# Chick Sorter v14.0 Zunda Tax / Resale
|
||||
# Chick Sorter v16.1 Card Target Overlay Fix
|
||||
|
||||
## Changes in this build
|
||||
- Changed Egg Farm build price to `¥250`.
|
||||
- Replaced the previous daily Fairies fee with `ZUNDA TAX`, paid when `Next Day` is pressed.
|
||||
- `ZUNDA TAX` taxes only cash above `¥500`: `ceil((cash - 500) × rate)`, with 5% added per `¥500` band and a 95% cap.
|
||||
- Removing equipment from a previous Build phase now sells it for 50% of its build price; same-Build removals still refund 100%.
|
||||
- Removed visible `STOPPED` text/highlight. Blocked chicks simply stay still.
|
||||
- Removed route-overlay arrows/colored flow arrows. Conveyor movement is now shown by small direction triangles inside belt tiles.
|
||||
- Moved the starter S1 position lower so the Egg Farm and S1 have more distance.
|
||||
- Moved the Truck to the bottom side of the grid and connected the starter S2-to-Truck belt to the new receiver.
|
||||
- Made scanner and facility ports strict: a belt must occupy the exact port cell to count as connected.
|
||||
- Expanded `Next Day` validation to scanner inputs/outputs and planned output routes, not just machine receiver cells.
|
||||
- Build buttons for unaffordable equipment, or already-built unique machines, are disabled and visually faded.
|
||||
- Removed Close buttons from clicked-equipment/upgrade popovers. Click outside the popover to close it.
|
||||
- Kept `src/index.html` and `src/styles.css` removed; the project uses the root `index.html` and `styles.css` only.
|
||||
- Continued centralized price/income/fine/facility definitions in `src/core/config.js`, shared text in `src/core/text.js`, and economy logic in `src/systems/economy.js`.
|
||||
This build refines the v15 card and tax system.
|
||||
|
||||
## Structure ideas for the next cleanup pass
|
||||
- Move HTML button content to a data-rendered build panel instead of hard-coded markup.
|
||||
- Give every equipment object a `defId`, so runtime state never duplicates names, prices, sizes, or upgrade rules.
|
||||
- Store routes as cell arrays and convert to pixel paths only at movement/draw time.
|
||||
- Cache route plans per build session, then invalidate once on build/erase/drag instead of recomputing every draw.
|
||||
- Split route validation into errors and warnings, so optional/unused equipment can be supported later without blocking the day.
|
||||
- Remove legacy fallback paths after save compatibility is no longer needed.
|
||||
## v16.1 changes
|
||||
- Fixed equipment improvement target-picking overlay by importing the target bounds helper into the renderer.
|
||||
- Non-candidate equipment now receives a full dark overlay, and valid upgrade candidates are explicitly white-highlighted.
|
||||
- Added a fallback message when a target picker has no valid target, so the game no longer appears frozen.
|
||||
|
||||
## v16 changes
|
||||
|
||||
- `Extra Cards` is now immediate.
|
||||
- Selecting it consumes the current card pick, then grants 2 more card selections in the same draft.
|
||||
- It no longer increases future draft size.
|
||||
- Equipment improvement cards now target machines on the map.
|
||||
- Valid targets are highlighted in white.
|
||||
- Non-target areas are darkened.
|
||||
- Click a highlighted machine to apply the upgrade.
|
||||
- `Esc` cancels target selection and returns to the card draft.
|
||||
- ZUNDA TAX and Fairies tribute no longer contaminate the next day’s profit display.
|
||||
- They are still paid when `Next Day` is pressed.
|
||||
- They still affect cash and total outflow.
|
||||
- They are not counted inside the new day’s `Turn Profit`.
|
||||
- ZUNDA TAX exemption / Legal Work behavior was rechecked and kept explicit.
|
||||
- Base exemption: ¥500.
|
||||
- `Legal Work`: exemption +¥250 and 95% cap point +¥250.
|
||||
- Tax rate: 5% from the first yen above the exemption.
|
||||
- +5% per ¥500 taxable band.
|
||||
- Maximum rate: 95%.
|
||||
- EGG FARM price remains ¥250.
|
||||
|
||||
## Active ZUNDA TAX formula
|
||||
|
||||
```text
|
||||
taxable = max(0, cash - exemption)
|
||||
rate = min(95%, ceil(taxable / 500) × 5%)
|
||||
ZUNDA TAX = ceil(taxable × rate)
|
||||
```
|
||||
|
||||
With no `Legal Work`, `exemption = ¥500`, so ¥501 starts at 5% and ¥10,000 reaches 95%.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Chick Sorter v14.0 Zunda Tax / Resale</title>
|
||||
<title>Chick Sorter v15.0 Cards / Tribute</title>
|
||||
<link rel="stylesheet" href="./styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -35,7 +35,7 @@
|
|||
<div id="buildPanel" class="build-panel" aria-label="Build and facility upgrades">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h1>CHICK SORTER <span>v14.0</span></h1>
|
||||
<h1>CHICK SORTER <span>v15.0</span></h1>
|
||||
<p>Build, drag, connect all receiver ports, then start the next day.</p>
|
||||
</div>
|
||||
<button id="nextTurnButton" class="primary-button" type="button">Next Day</button>
|
||||
|
|
@ -92,6 +92,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./src/game.js?v=14.0-zunda-tax-resale"></script>
|
||||
<script type="module" src="./src/game.js?v=15.0-cards-tribute"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const VERSION = 'v14.0 Zunda Tax / Resale';
|
||||
export const VERSION = 'v16.1 Card Target Overlay Fix';
|
||||
|
||||
export const TURN_SECONDS = 60;
|
||||
export const STARTING_CASH = 250;
|
||||
|
|
@ -11,10 +11,15 @@ export const ECONOMY = {
|
|||
poopFine: 10,
|
||||
zundaTax: {
|
||||
exemption: 500,
|
||||
legalWorkExemptionBonus: 250,
|
||||
maxRateCash: 10000,
|
||||
stepAmount: 500,
|
||||
stepRate: 0.05,
|
||||
maxRate: 0.95
|
||||
},
|
||||
fairiesTribute: {
|
||||
perDay: 10
|
||||
},
|
||||
incomeUpgradeRate: 1.05,
|
||||
explosionDamageDivisor: 30,
|
||||
maleTruckFinePerHalfDay: 30
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ export function newTurnStats() {
|
|||
mixerRevenue: 0,
|
||||
truckRevenue: 0,
|
||||
zundaTax: 0,
|
||||
fairiesTribute: 0,
|
||||
cardRerollCost: 0,
|
||||
mixerPoopFine: 0,
|
||||
truckPoopFine: 0,
|
||||
maleTruckFine: 0,
|
||||
|
|
@ -47,6 +49,8 @@ export function newTotalStats() {
|
|||
explosionDamage: 0,
|
||||
contractBonus: 0,
|
||||
zundaTax: 0,
|
||||
fairiesTribute: 0,
|
||||
cardRerollCost: 0,
|
||||
mixerPoopFine: 0,
|
||||
truckPoopFine: 0,
|
||||
maleTruckFine: 0,
|
||||
|
|
@ -92,6 +96,9 @@ export function createGame() {
|
|||
lastExplodedComponent: new Map(),
|
||||
contractOffer: null,
|
||||
contractActive: null,
|
||||
cardEffects: { bearing: 0, legalWork: 0, flattery: 0 },
|
||||
cardDraft: { pending: false, choices: [], rerolls: 0, picksRemaining: 0 },
|
||||
cardTargetPick: null,
|
||||
stats: newTurnStats(),
|
||||
lastResult: null,
|
||||
totals: newTotalStats()
|
||||
|
|
|
|||
29
src/game.js
29
src/game.js
|
|
@ -1,14 +1,15 @@
|
|||
import { TURN_SECONDS, CONVEYOR_SPEED, CONVEYOR_SPEED_MAX, THEME } from './core/config.js';
|
||||
import { TURN_SECONDS, THEME } from './core/config.js';
|
||||
import { buildToolButtonHtml } from './core/text.js';
|
||||
import { createGame, resetLayout, newTurnStats } from './core/state.js';
|
||||
import { clamp, pointToCell, cellCenter, yen } from './core/utils.js';
|
||||
import { facilityConnectionIssues } from './systems/routing.js';
|
||||
import { collectZundaTax, settleTruckRevenue } from './systems/economy.js';
|
||||
import { collectFairiesTribute, collectZundaTax, settleTruckRevenue } from './systems/economy.js';
|
||||
import { undo, redo } from './systems/history.js';
|
||||
import { drawAll } from './render/draw.js';
|
||||
import { createBuildSystem } from './systems/buildSystem.js';
|
||||
import { createUISystem } from './systems/uiSystem.js';
|
||||
import { createChickSystem } from './systems/chickSystem.js';
|
||||
import { conveyorSpeedForGame, createCardSystem } from './systems/cards.js';
|
||||
import { floating, shockwave, sparkBurst, updateEffects } from './systems/effects.js';
|
||||
import { rollContractOffer, activateAcceptedContract, clearActiveContract, resolveContract } from './systems/contracts.js';
|
||||
|
||||
|
|
@ -48,8 +49,9 @@ const game = createGame();
|
|||
let build;
|
||||
let uiSystem;
|
||||
let chicks;
|
||||
let cardSystem;
|
||||
|
||||
function currentConveyorSpeed() { return Math.min(CONVEYOR_SPEED_MAX, CONVEYOR_SPEED); }
|
||||
function currentConveyorSpeed() { return conveyorSpeedForGame(game); }
|
||||
|
||||
function startGame() {
|
||||
Object.assign(game, createGame());
|
||||
|
|
@ -64,6 +66,11 @@ function startGame() {
|
|||
|
||||
function startNextTurn() {
|
||||
if (game.phase !== 'build') return;
|
||||
if (game.cardDraft?.pending || game.cardTargetPick?.pending) {
|
||||
build.fail(game.cardTargetPick?.pending ? 'Choose highlighted upgrade target first.' : 'Choose an upgrade card first.');
|
||||
uiSystem.updatePanels();
|
||||
return;
|
||||
}
|
||||
const issues = facilityConnectionIssues(game);
|
||||
if (issues.length) {
|
||||
build.fail(`Cannot start: ${issues[0]}`);
|
||||
|
|
@ -71,6 +78,12 @@ function startNextTurn() {
|
|||
return;
|
||||
}
|
||||
const zundaBasisCash = game.cash;
|
||||
const zunda = collectZundaTax(game, zundaBasisCash);
|
||||
const tribute = collectFairiesTribute(game, game.turn);
|
||||
game.lastStartFees = { zundaTax: zunda.tax || 0, fairiesTribute: tribute.amount || 0 };
|
||||
if (zunda.tax > 0) floating(game, canvas.width / 2 - game.view.x, 116 - game.view.y, `ZUNDA TAX -${yen(zunda.tax)}`, THEME.danger);
|
||||
if (tribute.amount > 0) floating(game, canvas.width / 2 - game.view.x, 146 - game.view.y, `FAIRIES -${yen(tribute.amount)}`, THEME.danger);
|
||||
if (game.cash < 0) { game.phase = 'gameover'; uiSystem.showGameOver(); return; }
|
||||
activateAcceptedContract(game);
|
||||
game.phase = 'running';
|
||||
game.turn += 1;
|
||||
|
|
@ -83,8 +96,6 @@ function startNextTurn() {
|
|||
game.truckCargo = [];
|
||||
game.view = { x: 0, y: 0 };
|
||||
game.stats = newTurnStats();
|
||||
const zunda = collectZundaTax(game, zundaBasisCash);
|
||||
if (zunda.tax > 0) floating(game, canvas.width / 2 - game.view.x, 116 - game.view.y, `ZUNDA TAX -${yen(zunda.tax)}`, THEME.danger);
|
||||
for (const scanner of game.scanners) { scanner.queue = []; scanner.cooldown = 0; }
|
||||
for (const farm of game.eggFarms) { farm.nextSpawn = chicks.currentSpawnDelay(farm); farm.lastInterval = farm.nextSpawn; farm.shutterProgress = 0; farm.shutterSparked = false; }
|
||||
game.buildTool = null;
|
||||
|
|
@ -116,6 +127,8 @@ function completeTurn() {
|
|||
for (const scanner of game.scanners) scanner.queue = [];
|
||||
for (const farm of game.eggFarms) { farm.shutterProgress = 0; farm.shutterSparked = false; }
|
||||
game.contractOffer = rollContractOffer(game);
|
||||
if (cardSystem) cardSystem.prepareDraft();
|
||||
game.cardTargetPick = null;
|
||||
game.buildTool = null;
|
||||
game.groupDrag = null;
|
||||
game.selectionBox = null;
|
||||
|
|
@ -174,6 +187,7 @@ canvas.addEventListener('pointerdown', event => {
|
|||
const world = canvasPoint(event);
|
||||
if (event.button === 2) { startPan(event); return; }
|
||||
if (event.button !== 0) return;
|
||||
if (game.cardTargetPick?.pending) { cardSystem.chooseTargetAtPoint(world); return; }
|
||||
if (game.buildTool === 'erase') { build.eraseAtPoint(world); return; }
|
||||
if (game.buildTool) {
|
||||
const hit = build.equipmentAtPoint(world);
|
||||
|
|
@ -211,9 +225,10 @@ document.addEventListener('pointerdown', event => {
|
|||
ui.buttons.s1Left.addEventListener('click', () => chicks.sortSlot(0, 'left')); ui.buttons.s1Right.addEventListener('click', () => chicks.sortSlot(0, 'right')); ui.buttons.s2Left.addEventListener('click', () => chicks.sortSlot(1, 'left')); ui.buttons.s2Right.addEventListener('click', () => chicks.sortSlot(1, 'right'));
|
||||
ui.buttons.nextTurn.addEventListener('click', startNextTurn); ui.buttons.conveyor.addEventListener('click', () => build.setBuildTool('conveyor')); ui.buttons.eggFarm.addEventListener('click', () => build.setBuildTool('eggFarm')); ui.buttons.autoScanner.addEventListener('click', () => build.setBuildTool('autoScanner')); ui.buttons.manualScanner.addEventListener('click', () => build.setBuildTool('manualScanner')); ui.buttons.mixer.addEventListener('click', () => build.setBuildTool('mixer')); ui.buttons.trash.addEventListener('click', () => build.setBuildTool('trash')); ui.buttons.truck.addEventListener('click', () => build.setBuildTool('truck')); ui.buttons.erase.addEventListener('click', () => build.setBuildTool('erase'));
|
||||
ui.buttons.undo.addEventListener('click', () => { if (undo(game)) uiSystem.updatePanels(); }); ui.buttons.redo.addEventListener('click', () => { if (redo(game)) uiSystem.updatePanels(); });
|
||||
window.addEventListener('keydown', event => { if (event.repeat) return; const name = event.key.toLowerCase(); if (name === 'a') { event.preventDefault(); chicks.sortSlot(0, 'left'); } if (name === 'd') { event.preventDefault(); chicks.sortSlot(0, 'right'); } if (event.key === 'ArrowLeft') { event.preventDefault(); chicks.sortSlot(1, 'left'); } if (event.key === 'ArrowRight') { event.preventDefault(); chicks.sortSlot(1, 'right'); } if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && name === 'z') { event.preventDefault(); if (undo(game)) uiSystem.updatePanels(); } if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && (name === 'y' || (event.shiftKey && name === 'z'))) { event.preventDefault(); if (redo(game)) uiSystem.updatePanels(); } if (name === 'f1') { event.preventDefault(); game.debug = !game.debug; } });
|
||||
window.addEventListener('keydown', event => { if (event.repeat) return; const name = event.key.toLowerCase(); if (name === 'a') { event.preventDefault(); chicks.sortSlot(0, 'left'); } if (name === 'd') { event.preventDefault(); chicks.sortSlot(0, 'right'); } if (event.key === 'ArrowLeft') { event.preventDefault(); chicks.sortSlot(1, 'left'); } if (event.key === 'ArrowRight') { event.preventDefault(); chicks.sortSlot(1, 'right'); } if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && name === 'z') { event.preventDefault(); if (undo(game)) uiSystem.updatePanels(); } if (game.phase === 'build' && (event.ctrlKey || event.metaKey) && (name === 'y' || (event.shiftKey && name === 'z'))) { event.preventDefault(); if (redo(game)) uiSystem.updatePanels(); } if (event.key === 'Escape' && game.cardTargetPick?.pending) { event.preventDefault(); cardSystem.cancelTargetPick(); } if (name === 'f1') { event.preventDefault(); game.debug = !game.debug; } });
|
||||
|
||||
build = createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanels: () => uiSystem?.updatePanels() });
|
||||
chicks = createChickSystem({ game, currentConveyorSpeed, onGameOverCheck: () => uiSystem?.checkGameOver() });
|
||||
uiSystem = createUISystem({ game, ui, build, startGame, activeQueuedChick: chicks.activeQueuedChick });
|
||||
cardSystem = createCardSystem({ game, ui, onUpdatePanels: () => uiSystem?.updatePanels() });
|
||||
uiSystem = createUISystem({ game, ui, build, startGame, beginCardDraft: () => cardSystem.showDraft(), activeQueuedChick: chicks.activeQueuedChick });
|
||||
initializeStaticText(); resetLayout(game); chicks.updateCongestion(); uiSystem.updatePanels(); uiSystem.updateUI(); uiSystem.showTitle(); requestAnimationFrame(update);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { GRID, THEME, EFFECT_PRIORITY, VERSION } from '../core/config.js';
|
|||
import { key, parseKey, pointToCell, cellCenter, mixHex, randomBetween, yen } from '../core/utils.js';
|
||||
import { getConveyorNeighbors, routeFromFarmToScanner, outputRoute, scannerCenter, scannerConnector, scannerOutputs, destinationLabel, destinationColor, scannerPortHasConveyor } from '../systems/routing.js';
|
||||
import { upgradedMixerPrice, upgradedTruckPrice } from '../systems/economy.js';
|
||||
import { cardTargetBounds } from '../systems/cards.js';
|
||||
|
||||
const ASSET_PATHS = {
|
||||
chickMale: './assets/images/chick_male.png',
|
||||
|
|
@ -44,6 +45,7 @@ export function drawAll(ctx, canvas, game, helpers) {
|
|||
drawEggFarms(ctx, game);
|
||||
drawScanners(ctx, game);
|
||||
drawChicks(ctx, game, helpers.activeQueuedChick);
|
||||
drawCardTargetOverlay(ctx, canvas, game);
|
||||
drawSelectedTooltip(ctx, game, helpers.selectedObject, helpers.selectedTitle);
|
||||
drawSelectionBox(ctx, game);
|
||||
drawEffects(ctx, game);
|
||||
|
|
@ -445,6 +447,58 @@ function drawSelectedTooltip(ctx, game, selectedObject, selectedTitle) {
|
|||
else if (obj.type === 'facility') { x = obj.x + obj.w / 2 - 76; y = obj.y - 48; }
|
||||
ctx.save(); ctx.fillStyle = 'rgba(255,255,255,.95)'; ctx.strokeStyle = THEME.ink; ctx.lineWidth = 3; rect(ctx, x, y, 176, 40, true, true); ctx.fillStyle = THEME.ink; ctx.font = '900 11px ui-monospace, monospace'; ctx.fillText(selectedTitle(obj), x + 9, y + 16); ctx.fillStyle = THEME.muted; ctx.font = '900 9px ui-monospace, monospace'; ctx.fillText('drag to move / panel to edit', x + 9, y + 31); ctx.restore();
|
||||
}
|
||||
|
||||
function drawCardTargetOverlay(ctx, canvas, game) {
|
||||
if (!game.cardTargetPick?.pending) return;
|
||||
const items = cardTargetBounds(game);
|
||||
ctx.save();
|
||||
// The canvas is already translated by the camera. Offset the dark layer back to screen space
|
||||
// so every non-candidate object is reliably dimmed even after panning.
|
||||
ctx.fillStyle = 'rgba(0,0,0,.66)';
|
||||
ctx.fillRect(-game.view.x, -game.view.y, canvas.width, canvas.height);
|
||||
if (!items.length) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,.95)';
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = 3;
|
||||
rect(ctx, 260 - game.view.x, 96 - game.view.y, 420, 42, true, true);
|
||||
ctx.fillStyle = THEME.danger;
|
||||
ctx.font = '900 12px ui-monospace, monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('NO VALID UPGRADE TARGET. PRESS ESC.', 470 - game.view.x, 122 - game.view.y);
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
for (const item of items) {
|
||||
const b = item.bounds;
|
||||
ctx.save();
|
||||
ctx.shadowColor = THEME.white;
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.fillStyle = 'rgba(255,255,255,.32)';
|
||||
ctx.strokeStyle = THEME.white;
|
||||
ctx.lineWidth = 8;
|
||||
ctx.setLineDash([10, 6]);
|
||||
rect(ctx, b.x - 10, b.y - 10, b.w + 20, b.h + 20, true, true);
|
||||
ctx.restore();
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = THEME.green;
|
||||
ctx.lineWidth = 4;
|
||||
ctx.strokeRect(b.x - 4, b.y - 4, b.w + 8, b.h + 8);
|
||||
ctx.restore();
|
||||
|
||||
ctx.fillStyle = 'rgba(255,255,255,.96)';
|
||||
ctx.strokeStyle = THEME.ink;
|
||||
ctx.lineWidth = 3;
|
||||
const labelW = Math.min(300, Math.max(150, item.label.length * 7));
|
||||
rect(ctx, b.cx - labelW / 2, b.y - 34, labelW, 24, true, true);
|
||||
ctx.fillStyle = THEME.ink;
|
||||
ctx.font = '900 10px ui-monospace, monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(item.label, b.cx, b.y - 18);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawSelectionBox(ctx, game) {
|
||||
if (game.phase !== 'build' || !game.selectionBox) return;
|
||||
const b = game.selectionBox;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { AUTO_SCANNER_COOLDOWN, MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
|
||||
import { nextSpawnDelay, getSpawnRange } from '../core/state.js';
|
||||
import { MACHINE_FACILITY_IDS, GRID, THEME } from '../core/config.js';
|
||||
import { getSpawnRange } from '../core/state.js';
|
||||
import { createEggFarm, createScanner, createFacility } from '../core/entities.js';
|
||||
import { key, parseKey, pointToCell, cellCenter, yen } from '../core/utils.js';
|
||||
import { TEXT, equipmentName } from '../core/text.js';
|
||||
import { farmAt, scannerAt, scannerCenter, routeFromFarmToScanner, refreshRoutingAfterEdit } from './routing.js';
|
||||
import { buildPrice, equipmentBasePrice, incomeMultiplier, mixerPoopPenalty, refundCash, spendCash, truckPoopPenalty, upgradedMixerPrice, upgradedTruckPrice, upgradeCostFor, resaleValueFor } from './economy.js';
|
||||
import { buildPrice, equipmentBasePrice, incomeMultiplier, mixerPoopPenalty, refundCash, spendCash, truckPoopPenalty, upgradedMixerPrice, upgradedTruckPrice, resaleValueFor } from './economy.js';
|
||||
import { autoScannerCooldownSeconds } from './cards.js';
|
||||
import { record } from './history.js';
|
||||
import { floating, eraseEffect } from './effects.js';
|
||||
import { createSelectionSystem } from './selectionSystem.js';
|
||||
|
|
@ -198,23 +199,8 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
const selection = createSelectionSystem({ game, canvasPoint, updatePanels, equipmentAtPoint, fail, pointInGrid, rectOfFacility, rectsOverlap });
|
||||
|
||||
function selectedUpgradeCost(obj) {
|
||||
return upgradeCostFor(obj);
|
||||
}
|
||||
|
||||
function upgradeSelected() {
|
||||
const obj = selectedObject();
|
||||
const cost = selectedUpgradeCost(obj);
|
||||
if (!cost) return fail(TEXT.fail.noUpgrade);
|
||||
if (game.cash < cost) return fail(TEXT.fail.notEnoughCash);
|
||||
record(game);
|
||||
spendCash(game, cost);
|
||||
obj.level += 1;
|
||||
if (obj.type === 'eggFarm') {
|
||||
obj.nextSpawn = nextSpawnDelay(obj);
|
||||
obj.lastInterval = obj.nextSpawn;
|
||||
}
|
||||
updatePanels();
|
||||
function selectedUpgradeCost(_obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function removeSelected() {
|
||||
|
|
@ -243,7 +229,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
|
||||
function showAutoScannerMenu(scanner) {
|
||||
ui.modalTitle.textContent = 'Auto Scanner';
|
||||
ui.modalBody.innerHTML = `<p>Standard auto scanner.</p><ul><li>Role 0: male left, others right.</li><li>Role 1: poop left, others right.</li><li>Cooldown: ${AUTO_SCANNER_COOLDOWN}s.</li></ul>`;
|
||||
ui.modalBody.innerHTML = `<p>Standard auto scanner.</p><ul><li>Role 0: male left, others right.</li><li>Role 1: poop left, others right.</li><li>Cooldown: ${autoScannerCooldownSeconds(scanner).toFixed(1)}s.</li></ul>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
const r0 = modalButton('Set Role 0', () => { record(game); scanner.role = 0; hideModal(); updatePanels(); });
|
||||
const r1 = modalButton('Set Role 1', () => { record(game); scanner.role = 1; hideModal(); updatePanels(); });
|
||||
|
|
@ -288,13 +274,6 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
<div class="equipment-menu-lines compact">${lines.map(x => `<p>${x}</p>`).join('')}</div>
|
||||
${['mixer', 'truck'].includes(obj.id) ? '<p class="formula-box compact">Income/Fine = ceil(base × 1.05^upgrades)</p>' : ''}`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
const cost = selectedUpgradeCost(obj);
|
||||
if (cost) {
|
||||
const upgrade = modalButton(`Upgrade ${yen(cost)}`, () => { upgradeSelected(); hideModal(); }, 'primary-button');
|
||||
upgrade.disabled = game.cash < cost;
|
||||
if (upgrade.disabled) upgrade.title = TEXT.fail.notEnoughCash;
|
||||
ui.modalActions.appendChild(upgrade);
|
||||
}
|
||||
if (obj.type === 'scanner' && obj.kind === 'auto') ui.modalActions.appendChild(modalButton('Auto Menu', () => showAutoScannerMenu(obj), 'facility-action warn'));
|
||||
const hit = obj.type === 'conveyor' ? { type: 'conveyor', oldKey: obj.id, ref: obj } : { type: obj.type, ref: obj };
|
||||
const resale = resaleValueFor(hit, game);
|
||||
|
|
@ -339,7 +318,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
lines.push(`Type: ${obj.kind.toUpperCase()} / Standard`);
|
||||
lines.push(`Role: ${obj.role === 0 ? 'Male left / Others right' : 'Poop left / Others right'}`);
|
||||
lines.push(`Queue: ${obj.queue.length}`);
|
||||
if (obj.kind === 'auto') lines.push(`Cooldown: ${obj.cooldown.toFixed(1)}s / ${AUTO_SCANNER_COOLDOWN.toFixed(1)}s`);
|
||||
if (obj.kind === 'auto') lines.push(`Cooldown: ${obj.cooldown.toFixed(1)}s / ${autoScannerCooldownSeconds(obj).toFixed(1)}s`);
|
||||
|
||||
} else if (obj.type === 'conveyor') {
|
||||
const meta = game.conveyorMeta.get(obj.id);
|
||||
|
|
@ -377,7 +356,7 @@ export function createBuildSystem({ game, ui, canvas, canvasPoint, onUpdatePanel
|
|||
finishGroupDrag: selection.finishGroupDrag,
|
||||
selectedObject, selectedTitle, equipmentPrice, selectedUpgradeCost,
|
||||
selectedInfoLines,
|
||||
upgradeSelected, removeSelected, switchScannerRole,
|
||||
removeSelected, switchScannerRole,
|
||||
showAutoScannerMenu, showSelectedMenu, setBuildTool, fail,
|
||||
isEquipmentCell, equipmentHitBoxes,
|
||||
routeFromFarmToScanner
|
||||
|
|
|
|||
393
src/systems/cards.js
Normal file
393
src/systems/cards.js
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
import { AUTO_SCANNER_COOLDOWN, CONVEYOR_SPEED, CONVEYOR_SPEED_MAX, GRID, THEME } from '../core/config.js';
|
||||
import { nextSpawnDelay } from '../core/state.js';
|
||||
import { cellCenter, yen } from '../core/utils.js';
|
||||
import { applyPenalty, upgradedMixerPrice, upgradedTruckPrice } from './economy.js';
|
||||
import { floating } from './effects.js';
|
||||
import { scannerCenter } from './routing.js';
|
||||
|
||||
const COMMON_WEIGHT = 8;
|
||||
const RARE_WEIGHT = 2;
|
||||
const AUTO_SCANNER_COOLDOWN_RATE = 0.95;
|
||||
const AUTO_SCANNER_MIN_COOLDOWN = 1.0;
|
||||
const BASE_DRAFT_SIZE = 3;
|
||||
|
||||
export const CARD_DEFS = [
|
||||
{
|
||||
id: 'upgradeEgg',
|
||||
title: 'EGG Improvement',
|
||||
rarity: 'common',
|
||||
type: 'equipmentUpgrade',
|
||||
target: 'eggFarm',
|
||||
description: 'Choose one EGG FARM on the map and raise it by 1 level.'
|
||||
},
|
||||
{
|
||||
id: 'upgradeAutoScanner',
|
||||
title: 'AUTO SCANNER Improvement',
|
||||
rarity: 'common',
|
||||
type: 'equipmentUpgrade',
|
||||
target: 'autoScanner',
|
||||
description: 'Choose one AUTO SCANNER on the map and raise it by 1 level.'
|
||||
},
|
||||
{
|
||||
id: 'upgradeMixer',
|
||||
title: 'MIXER Improvement',
|
||||
rarity: 'common',
|
||||
type: 'equipmentUpgrade',
|
||||
target: 'mixer',
|
||||
description: 'Choose MIXER on the map and raise it by 1 level.'
|
||||
},
|
||||
{
|
||||
id: 'upgradeTruck',
|
||||
title: 'TRUCK Improvement',
|
||||
rarity: 'common',
|
||||
type: 'equipmentUpgrade',
|
||||
target: 'truck',
|
||||
description: 'Choose TRUCK on the map and raise it by 1 level.'
|
||||
},
|
||||
{
|
||||
id: 'bearing',
|
||||
title: 'High-Quality Bearing',
|
||||
rarity: 'common',
|
||||
type: 'instant',
|
||||
description: 'Conveyor speed +2%.'
|
||||
},
|
||||
{
|
||||
id: 'extraCards',
|
||||
title: 'Extra Cards',
|
||||
rarity: 'rare',
|
||||
type: 'instant',
|
||||
description: 'Draw 2 cards now and choose both.'
|
||||
},
|
||||
{
|
||||
id: 'legalWork',
|
||||
title: 'Legal Work',
|
||||
rarity: 'rare',
|
||||
type: 'instant',
|
||||
description: 'ZUNDA TAX exemption +¥250. The 95% cap point also moves up by ¥250.'
|
||||
},
|
||||
{
|
||||
id: 'flattery',
|
||||
title: 'Flattery',
|
||||
rarity: 'rare',
|
||||
type: 'instant',
|
||||
description: 'Fairies tribute reduction card.'
|
||||
}
|
||||
];
|
||||
|
||||
export function ensureCardState(game) {
|
||||
if (!game.cardEffects) game.cardEffects = { bearing: 0, legalWork: 0, flattery: 0 };
|
||||
if (game.cardEffects.drawBonus != null) delete game.cardEffects.drawBonus;
|
||||
if (!game.cardDraft) game.cardDraft = { pending: false, choices: [], rerolls: 0, picksRemaining: 0 };
|
||||
if (game.cardDraft.picksRemaining == null) game.cardDraft.picksRemaining = game.cardDraft.pending ? 1 : 0;
|
||||
return game.cardEffects;
|
||||
}
|
||||
|
||||
export function conveyorSpeedForGame(game) {
|
||||
const effects = ensureCardState(game);
|
||||
const speed = CONVEYOR_SPEED * Math.pow(1.02, Math.max(0, effects.bearing || 0));
|
||||
return Math.min(CONVEYOR_SPEED_MAX, speed);
|
||||
}
|
||||
|
||||
export function autoScannerCooldownSeconds(scanner) {
|
||||
const upgrades = Math.max(0, (scanner?.level || 1) - 1);
|
||||
return Math.max(AUTO_SCANNER_MIN_COOLDOWN, AUTO_SCANNER_COOLDOWN * Math.pow(AUTO_SCANNER_COOLDOWN_RATE, upgrades));
|
||||
}
|
||||
|
||||
export function flatteryReductionForTurn(game, turn = game.turn) {
|
||||
const effects = ensureCardState(game);
|
||||
const day = Math.max(1, Math.floor(Number(turn) || 1));
|
||||
const perCard = Math.floor(10 * day / 10);
|
||||
return Math.max(0, perCard * Math.max(0, effects.flattery || 0));
|
||||
}
|
||||
|
||||
export function rerollCost(game) {
|
||||
const draft = game.cardDraft || { rerolls: 0 };
|
||||
const nextRerollCount = Math.max(1, (draft.rerolls || 0) + 1);
|
||||
return Math.ceil((Math.max(1, game.turn || 1) / 2) * nextRerollCount * 100);
|
||||
}
|
||||
|
||||
export function cardById(id) {
|
||||
return CARD_DEFS.find(card => card.id === id) || null;
|
||||
}
|
||||
|
||||
function targetLabel(game, target) {
|
||||
if (target.type === 'eggFarm') {
|
||||
return `EGG #${target.id} L${target.level} -> L${target.level + 1}`;
|
||||
}
|
||||
if (target.type === 'scanner') {
|
||||
return `AUTO #${target.id} L${target.level} -> L${target.level + 1} | CD ${autoScannerCooldownSeconds(target).toFixed(1)}s -> ${autoScannerCooldownSeconds({ ...target, level: target.level + 1 }).toFixed(1)}s`;
|
||||
}
|
||||
if (target.type === 'facility' && target.id === 'mixer') {
|
||||
const beforeLevel = target.level;
|
||||
const before = upgradedMixerPrice(game);
|
||||
target.level = beforeLevel + 1;
|
||||
const after = upgradedMixerPrice(game);
|
||||
target.level = beforeLevel;
|
||||
return `MIXER L${target.level} -> L${target.level + 1} | ${yen(before)} -> ${yen(after)}`;
|
||||
}
|
||||
if (target.type === 'facility' && target.id === 'truck') {
|
||||
const beforeLevel = target.level;
|
||||
const before = upgradedTruckPrice(game);
|
||||
target.level = beforeLevel + 1;
|
||||
const after = upgradedTruckPrice(game);
|
||||
target.level = beforeLevel;
|
||||
return `TRUCK L${target.level} -> L${target.level + 1} | ${yen(before)} -> ${yen(after)}`;
|
||||
}
|
||||
return `${target.name || target.id} L${target.level || 1} -> L${(target.level || 1) + 1}`;
|
||||
}
|
||||
|
||||
export function targetsForCard(game, cardOrId) {
|
||||
const card = typeof cardOrId === 'string' ? cardById(cardOrId) : cardOrId;
|
||||
ensureCardState(game);
|
||||
if (!card) return [];
|
||||
if (card.target === 'eggFarm') return game.eggFarms.filter(f => (f.level || 1) < 4);
|
||||
if (card.target === 'autoScanner') return game.scanners.filter(s => s.kind === 'auto');
|
||||
if (card.target === 'mixer') return game.facilities.mixer ? [game.facilities.mixer] : [];
|
||||
if (card.target === 'truck') return game.facilities.truck ? [game.facilities.truck] : [];
|
||||
return [];
|
||||
}
|
||||
|
||||
function availableCards(game) {
|
||||
return CARD_DEFS.filter(card => card.type !== 'equipmentUpgrade' || targetsForCard(game, card).length > 0);
|
||||
}
|
||||
|
||||
function weightedPick(pool) {
|
||||
const total = pool.reduce((sum, card) => sum + (card.rarity === 'rare' ? RARE_WEIGHT : COMMON_WEIGHT), 0);
|
||||
let roll = Math.random() * total;
|
||||
for (const card of pool) {
|
||||
roll -= card.rarity === 'rare' ? RARE_WEIGHT : COMMON_WEIGHT;
|
||||
if (roll <= 0) return card;
|
||||
}
|
||||
return pool[pool.length - 1];
|
||||
}
|
||||
|
||||
export function dealCards(game, count = BASE_DRAFT_SIZE) {
|
||||
ensureCardState(game);
|
||||
const source = availableCards(game);
|
||||
if (!source.length) return [];
|
||||
const choices = [];
|
||||
let pool = [...source];
|
||||
while (choices.length < count && source.length) {
|
||||
if (!pool.length) pool = [...source];
|
||||
const picked = weightedPick(pool);
|
||||
choices.push(picked);
|
||||
const i = pool.findIndex(card => card.id === picked.id);
|
||||
if (i >= 0) pool.splice(i, 1);
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
|
||||
function targetKey(target) {
|
||||
if (!target) return '';
|
||||
if (target.type === 'eggFarm') return `eggFarm:${target.id}`;
|
||||
if (target.type === 'scanner') return `scanner:${target.id}`;
|
||||
if (target.type === 'facility') return `facility:${target.id}`;
|
||||
return `${target.type}:${target.id}`;
|
||||
}
|
||||
|
||||
function boundsForTarget(target) {
|
||||
if (!target) return null;
|
||||
if (target.type === 'eggFarm') {
|
||||
const c = cellCenter(target.col, target.row);
|
||||
return { x: c.x - 34, y: c.y - 34, w: 68, h: 68, cx: c.x, cy: c.y };
|
||||
}
|
||||
if (target.type === 'scanner') {
|
||||
const c = scannerCenter(target);
|
||||
return { x: c.x - 58, y: c.y - 42, w: 116, h: 84, cx: c.x, cy: c.y };
|
||||
}
|
||||
if (target.type === 'facility') {
|
||||
return { x: target.x, y: target.y, w: target.w, h: target.h, cx: target.x + target.w / 2, cy: target.y + target.h / 2 };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function cardTargetBounds(game) {
|
||||
if (!game.cardTargetPick?.pending) return [];
|
||||
const allowed = new Set(game.cardTargetPick.targetKeys || []);
|
||||
return targetsForCard(game, game.cardTargetPick.cardId)
|
||||
.filter(target => allowed.has(targetKey(target)))
|
||||
.map(target => ({ key: targetKey(target), label: targetLabel(game, target), target, bounds: boundsForTarget(target) }))
|
||||
.filter(item => item.bounds);
|
||||
}
|
||||
|
||||
function targetAtPoint(game, p) {
|
||||
const items = cardTargetBounds(game);
|
||||
for (const item of items) {
|
||||
const b = item.bounds;
|
||||
if (p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h) return item.target;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function applyEquipmentUpgrade(game, target) {
|
||||
target.level = (target.level || 1) + 1;
|
||||
if (target.type === 'eggFarm') {
|
||||
target.nextSpawn = nextSpawnDelay(target);
|
||||
target.lastInterval = target.nextSpawn;
|
||||
const c = cellCenter(target.col, target.row);
|
||||
floating(game, c.x, c.y - 30, `LV ${target.level}`, THEME.green);
|
||||
return;
|
||||
}
|
||||
if (target.type === 'scanner') {
|
||||
const c = scannerCenter(target);
|
||||
floating(game, c.x, c.y - 34, `LV ${target.level}`, THEME.green);
|
||||
return;
|
||||
}
|
||||
if (target.type === 'facility') {
|
||||
floating(game, target.x + target.w / 2, target.y + 18, `LV ${target.level}`, THEME.green);
|
||||
}
|
||||
}
|
||||
|
||||
function applyInstantCard(game, card) {
|
||||
const effects = ensureCardState(game);
|
||||
if (card.id === 'bearing') effects.bearing += 1;
|
||||
if (card.id === 'legalWork') effects.legalWork += 1;
|
||||
if (card.id === 'flattery') effects.flattery += 1;
|
||||
}
|
||||
|
||||
function cardDescription(game, card) {
|
||||
if (card.id === 'flattery') {
|
||||
const reduction = flatteryReductionForTurn({ ...game, cardEffects: { ...ensureCardState(game), flattery: 1 } }, game.turn);
|
||||
return `Fairies tribute reduction: ${yen(reduction)}`;
|
||||
}
|
||||
return card.description;
|
||||
}
|
||||
|
||||
function removeOneChoice(game, card) {
|
||||
const choices = game.cardDraft?.choices || [];
|
||||
const index = choices.findIndex(choice => choice.id === card.id);
|
||||
if (index >= 0) choices.splice(index, 1);
|
||||
}
|
||||
|
||||
export function createCardSystem({ game, ui, onUpdatePanels }) {
|
||||
function updatePanels() { if (onUpdatePanels) onUpdatePanels(); }
|
||||
|
||||
function prepareDraft() {
|
||||
ensureCardState(game);
|
||||
game.cardTargetPick = null;
|
||||
game.cardDraft = { pending: true, choices: dealCards(game, BASE_DRAFT_SIZE), rerolls: 0, picksRemaining: 1 };
|
||||
}
|
||||
|
||||
function finishDraft() {
|
||||
ensureCardState(game);
|
||||
game.cardDraft.pending = false;
|
||||
game.cardDraft.choices = [];
|
||||
game.cardDraft.picksRemaining = 0;
|
||||
game.cardTargetPick = null;
|
||||
ui.modal.classList.remove('visible', 'equipment-popover');
|
||||
updatePanels();
|
||||
}
|
||||
|
||||
function redrawChoices(count = BASE_DRAFT_SIZE) {
|
||||
ensureCardState(game);
|
||||
game.cardDraft.choices = dealCards(game, count);
|
||||
}
|
||||
|
||||
function refillChoicesIfNeeded() {
|
||||
ensureCardState(game);
|
||||
if (!game.cardDraft.choices.length && game.cardDraft.picksRemaining > 0) redrawChoices(BASE_DRAFT_SIZE);
|
||||
}
|
||||
|
||||
function completePickedCard(card) {
|
||||
removeOneChoice(game, card);
|
||||
game.cardDraft.picksRemaining = Math.max(0, (game.cardDraft.picksRemaining || 1) - 1);
|
||||
game.cardTargetPick = null;
|
||||
refillChoicesIfNeeded();
|
||||
if (game.cardDraft.picksRemaining <= 0) finishDraft();
|
||||
else showDraft();
|
||||
}
|
||||
|
||||
function chooseExtraCards(card) {
|
||||
removeOneChoice(game, card);
|
||||
game.cardDraft.picksRemaining = Math.max(0, (game.cardDraft.picksRemaining || 1) - 1) + 2;
|
||||
game.cardDraft.choices.push(...dealCards(game, 2));
|
||||
showDraft();
|
||||
}
|
||||
|
||||
function chooseCard(card) {
|
||||
if (card.id === 'extraCards') return chooseExtraCards(card);
|
||||
if (card.type === 'equipmentUpgrade') return startMapTargetPicker(card);
|
||||
applyInstantCard(game, card);
|
||||
completePickedCard(card);
|
||||
}
|
||||
|
||||
function startMapTargetPicker(card) {
|
||||
const targets = targetsForCard(game, card);
|
||||
if (!targets.length) return;
|
||||
game.cardTargetPick = {
|
||||
pending: true,
|
||||
cardId: card.id,
|
||||
targetKeys: targets.map(targetKey)
|
||||
};
|
||||
ui.modal.classList.remove('visible', 'equipment-popover');
|
||||
updatePanels();
|
||||
}
|
||||
|
||||
function chooseTargetAtPoint(p) {
|
||||
if (!game.cardTargetPick?.pending) return false;
|
||||
const card = cardById(game.cardTargetPick.cardId);
|
||||
const target = targetAtPoint(game, p);
|
||||
if (!card || !target) {
|
||||
floating(game, p.x, p.y - 18, 'SELECT WHITE TARGET', THEME.danger);
|
||||
return true;
|
||||
}
|
||||
applyEquipmentUpgrade(game, target);
|
||||
completePickedCard(card);
|
||||
return true;
|
||||
}
|
||||
|
||||
function cancelTargetPick() {
|
||||
if (!game.cardTargetPick?.pending) return;
|
||||
game.cardTargetPick = null;
|
||||
showDraft();
|
||||
}
|
||||
|
||||
function doReroll() {
|
||||
const cost = rerollCost(game);
|
||||
if (game.cash < cost) return;
|
||||
applyPenalty(game, cost);
|
||||
game.stats.cardRerollCost = (game.stats.cardRerollCost || 0) + cost;
|
||||
game.totals.cardRerollCost = (game.totals.cardRerollCost || 0) + cost;
|
||||
game.cardDraft.rerolls += 1;
|
||||
redrawChoices(BASE_DRAFT_SIZE);
|
||||
showDraft();
|
||||
}
|
||||
|
||||
function cardButton(card, index) {
|
||||
const b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = `card-choice ${card.rarity === 'rare' ? 'rare' : 'common'}`;
|
||||
const rarity = card.rarity === 'rare' ? 'RARE' : 'COMMON';
|
||||
b.innerHTML = `<strong>${card.title}</strong><span>${rarity}</span><small>${cardDescription(game, card)}</small>`;
|
||||
b.addEventListener('click', () => chooseCard(card));
|
||||
b.dataset.index = String(index);
|
||||
return b;
|
||||
}
|
||||
|
||||
function showDraft() {
|
||||
ensureCardState(game);
|
||||
game.cardTargetPick = null;
|
||||
if (!game.cardDraft?.pending) prepareDraft();
|
||||
if (!game.cardDraft.choices?.length) redrawChoices(BASE_DRAFT_SIZE);
|
||||
const choices = game.cardDraft.choices || [];
|
||||
const cost = rerollCost(game);
|
||||
const remaining = Math.max(1, game.cardDraft.picksRemaining || 1);
|
||||
ui.modalTitle.textContent = 'Choose Upgrade Card';
|
||||
ui.modalBody.innerHTML = `<p class="muted-card-note">Choose ${remaining} card${remaining === 1 ? '' : 's'} before Build phase. Equipment cards are applied by selecting a white-highlighted machine on the map.</p><div id="cardChoices" class="card-choices"></div>`;
|
||||
const wrap = ui.modalBody.querySelector('#cardChoices');
|
||||
choices.forEach((card, index) => wrap.appendChild(cardButton(card, index)));
|
||||
ui.modalActions.innerHTML = '';
|
||||
const reroll = document.createElement('button');
|
||||
reroll.type = 'button';
|
||||
reroll.className = 'facility-action warn';
|
||||
reroll.textContent = `Reroll ${yen(cost)}`;
|
||||
reroll.disabled = game.cash < cost;
|
||||
reroll.title = reroll.disabled ? 'Not enough cash' : `Reroll count today: ${(game.cardDraft.rerolls || 0) + 1}`;
|
||||
reroll.addEventListener('click', doReroll);
|
||||
ui.modalActions.appendChild(reroll);
|
||||
ui.modal.classList.remove('equipment-popover');
|
||||
ui.modal.classList.add('visible');
|
||||
}
|
||||
|
||||
return { prepareDraft, showDraft, finishDraft, chooseTargetAtPoint, cancelTargetPick };
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { AUTO_SCANNER_COOLDOWN, GRID, THEME } from '../core/config.js';
|
||||
import { GRID, THEME } from '../core/config.js';
|
||||
import { createChick } from '../core/entities.js';
|
||||
import { nextSpawnDelay } from '../core/state.js';
|
||||
import { key, parseKey, pointToCell, cellCenter, randomBetween, yen } from '../core/utils.js';
|
||||
import { scannerById, scannerBySlot, scannerCenter, routeFromFarmToScanner, outputRoute, destinationLabel, destinationColor, nearestConveyorKey, buildConveyorComponents, autoSideFor } from './routing.js';
|
||||
import { applyMixerIncome, applyMixerPoopFine, applyTruckPoopFine, applyWrongTruckFine, upgradedTruckPrice } from './economy.js';
|
||||
import { autoScannerCooldownSeconds } from './cards.js';
|
||||
import { productionMultiplier, truckTarget, isTargetTruckCargo, shouldFineMaleTruck } from './contracts.js';
|
||||
import { floating, shake, spawnPulse, scannerPulse, meatEffect, sludgeEffect, shredEffect, truckLoadEffect, shockwave, sparkBurst, smokeBurst, flyingDebris, rageEffect } from './effects.js';
|
||||
import { countAutoSorted, countCorrect, countMistake, countMixer, countPoopDestination, countPoopSpawned, countTrash, countTruckCargo } from './stats.js';
|
||||
|
|
@ -184,7 +185,7 @@ export function createChickSystem({ game, currentConveyorSpeed, onGameOverCheck
|
|||
continue;
|
||||
}
|
||||
if (sortChickByIndex(index, autoSideFor(scanner, game.chicks[index]), true)) {
|
||||
scanner.cooldown = AUTO_SCANNER_COOLDOWN;
|
||||
scanner.cooldown = autoScannerCooldownSeconds(scanner);
|
||||
countAutoSorted(game);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,19 +44,32 @@ export function maleTruckPenalty(game) {
|
|||
return Math.ceil(baseFine * incomeMultiplier(game, 'truck'));
|
||||
}
|
||||
|
||||
export function zundaTaxInfo(cash) {
|
||||
export function zundaTaxInfo(cash, game = null) {
|
||||
const rules = ECONOMY.zundaTax;
|
||||
const legalWork = Math.max(0, game?.cardEffects?.legalWork || 0);
|
||||
const exemption = rules.exemption + legalWork * rules.legalWorkExemptionBonus;
|
||||
const maxRateCash = rules.maxRateCash + legalWork * rules.legalWorkExemptionBonus;
|
||||
const profit = Math.max(0, Math.floor(Number(cash) || 0));
|
||||
const taxable = Math.max(0, profit - rules.exemption);
|
||||
if (taxable <= 0) return { profit, taxable: 0, rate: 0, ratePercent: 0, tax: 0, step: 0 };
|
||||
const taxable = Math.max(0, profit - exemption);
|
||||
if (taxable <= 0) return { profit, exemption, maxRateCash, taxable: 0, rate: 0, ratePercent: 0, tax: 0, step: 0, legalWork };
|
||||
const step = Math.max(1, Math.ceil(taxable / rules.stepAmount));
|
||||
const rate = Math.min(rules.maxRate, step * rules.stepRate);
|
||||
const tax = Math.ceil(taxable * rate);
|
||||
return { profit, taxable, rate, ratePercent: Math.round(rate * 100), tax, step };
|
||||
return { profit, exemption, maxRateCash, taxable, rate, ratePercent: Math.round(rate * 100), tax, step, legalWork };
|
||||
}
|
||||
|
||||
export function zundaTaxForCash(cash) {
|
||||
return zundaTaxInfo(cash).tax;
|
||||
export function zundaTaxForCash(cash, game = null) {
|
||||
return zundaTaxInfo(cash, game).tax;
|
||||
}
|
||||
|
||||
export function fairiesTributeInfo(game, turn = game.turn) {
|
||||
const day = Math.max(1, Math.floor(Number(turn) || 1));
|
||||
const base = Math.ceil(day * ECONOMY.fairiesTribute.perDay);
|
||||
const flatteryCount = Math.max(0, game?.cardEffects?.flattery || 0);
|
||||
const perCardReduction = Math.floor(10 * day / 10);
|
||||
const reduction = Math.min(base, flatteryCount * perCardReduction);
|
||||
const amount = Math.max(0, base - reduction);
|
||||
return { day, base, flatteryCount, perCardReduction, reduction, amount };
|
||||
}
|
||||
|
||||
export function resaleValueFor(hit, game) {
|
||||
|
|
@ -171,7 +184,7 @@ export function settleTruckRevenue(game) {
|
|||
}
|
||||
|
||||
export function collectZundaTax(game, basisCash = game.cash) {
|
||||
const info = zundaTaxInfo(basisCash);
|
||||
const info = zundaTaxInfo(basisCash, game);
|
||||
if (info.tax <= 0) return info;
|
||||
game.stats.zundaTax += info.tax;
|
||||
game.totals.zundaTax += info.tax;
|
||||
|
|
@ -179,6 +192,15 @@ export function collectZundaTax(game, basisCash = game.cash) {
|
|||
return info;
|
||||
}
|
||||
|
||||
export function collectFairiesTribute(game, turn = game.turn) {
|
||||
const info = fairiesTributeInfo(game, turn);
|
||||
if (info.amount <= 0) return info;
|
||||
game.stats.fairiesTribute = (game.stats.fairiesTribute || 0) + info.amount;
|
||||
game.totals.fairiesTribute = (game.totals.fairiesTribute || 0) + info.amount;
|
||||
applyPenalty(game, info.amount);
|
||||
return info;
|
||||
}
|
||||
|
||||
export function finalScore(game) {
|
||||
const value = factoryValue(game);
|
||||
const days = Math.max(1, game.totals.turnsCompleted || game.turn || 1);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ export function snapshot(game) {
|
|||
scanners: game.scanners.map(cleanScanner),
|
||||
conveyorTiles: [...game.conveyorTiles],
|
||||
conveyorMeta: [...game.conveyorMeta.entries()],
|
||||
branchCounters: [...game.branchCounters.entries()]
|
||||
branchCounters: [...game.branchCounters.entries()],
|
||||
cardEffects: game.cardEffects,
|
||||
cardDraft: game.cardDraft,
|
||||
cardTargetPick: game.cardTargetPick
|
||||
});
|
||||
}
|
||||
export function restore(game, text) {
|
||||
|
|
@ -35,6 +38,11 @@ export function restore(game, text) {
|
|||
game.conveyorTiles = new Set(data.conveyorTiles || []);
|
||||
game.conveyorMeta = new Map(data.conveyorMeta || []);
|
||||
game.branchCounters = new Map(data.branchCounters || []);
|
||||
game.cardEffects = data.cardEffects || { bearing: 0, legalWork: 0, flattery: 0 };
|
||||
if (game.cardEffects.drawBonus != null) delete game.cardEffects.drawBonus;
|
||||
game.cardDraft = data.cardDraft || { pending: false, choices: [], rerolls: 0, picksRemaining: 0 };
|
||||
if (game.cardDraft.picksRemaining == null) game.cardDraft.picksRemaining = game.cardDraft.pending ? 1 : 0;
|
||||
game.cardTargetPick = data.cardTargetPick || null;
|
||||
game.groupDrag = null;
|
||||
game.selectionBox = null;
|
||||
game.pan = null;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { BUILD_TOOL_IDS, CONVEYOR_SPEED, CONVEYOR_SPEED_MAX, CONTRACT_EVENT_FIRST_TURN, FACILITY_DEFS, MACHINE_FACILITY_IDS, STARTING_CASH } from '../core/config.js';
|
||||
import { BUILD_TOOL_IDS, CONVEYOR_SPEED_MAX, CONTRACT_EVENT_FIRST_TURN, FACILITY_DEFS, MACHINE_FACILITY_IDS, STARTING_CASH } from '../core/config.js';
|
||||
import { yen } from '../core/utils.js';
|
||||
import { TEXT } from '../core/text.js';
|
||||
import { routeFromFarmToScanner, factoryReady, facilityConnectionIssues } from './routing.js';
|
||||
import { buildPrice, finalScore, maleTruckPenalty, mixerPoopPenalty, truckPoopPenalty, upgradedMixerPrice, zundaTaxInfo } from './economy.js';
|
||||
import { buildPrice, fairiesTributeInfo, finalScore, maleTruckPenalty, mixerPoopPenalty, truckPoopPenalty, upgradedMixerPrice, zundaTaxInfo } from './economy.js';
|
||||
import { truckTarget } from './contracts.js';
|
||||
import { conveyorSpeedForGame } from './cards.js';
|
||||
|
||||
export function createUISystem({ game, ui, build, startGame, activeQueuedChick }) {
|
||||
function displaySpeed() { return Math.round(Math.min(CONVEYOR_SPEED_MAX, CONVEYOR_SPEED)); }
|
||||
export function createUISystem({ game, ui, build, startGame, beginCardDraft, activeQueuedChick }) {
|
||||
function displaySpeed() { return Math.round(conveyorSpeedForGame(game)); }
|
||||
|
||||
function updatePanels() {
|
||||
updateToolButtons();
|
||||
|
|
@ -28,7 +29,7 @@ export function createUISystem({ game, ui, build, startGame, activeQueuedChick }
|
|||
const price = buildPrice(id);
|
||||
const uniqueAlreadyBuilt = MACHINE_FACILITY_IDS.includes(id) && !!game.facilities[id];
|
||||
const unaffordable = game.cash < price;
|
||||
btn.disabled = game.phase !== 'build' || unaffordable || uniqueAlreadyBuilt;
|
||||
btn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || unaffordable || uniqueAlreadyBuilt;
|
||||
if (btn.disabled && game.buildTool === id) game.buildTool = null;
|
||||
btn.classList.toggle('unaffordable', unaffordable);
|
||||
btn.classList.toggle('already-built', uniqueAlreadyBuilt);
|
||||
|
|
@ -38,12 +39,12 @@ export function createUISystem({ game, ui, build, startGame, activeQueuedChick }
|
|||
? `${FACILITY_DEFS[id]?.name || id} already exists`
|
||||
: '';
|
||||
}
|
||||
if (ui.buttons.erase) ui.buttons.erase.disabled = game.phase !== 'build';
|
||||
if (ui.buttons.erase) ui.buttons.erase.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending;
|
||||
}
|
||||
|
||||
function updateBuildStatus() {
|
||||
ui.buildStatus.textContent = game.phase === 'build'
|
||||
? TEXT.status.build(game.buildTool)
|
||||
? (game.cardTargetPick?.pending ? 'Select a white-highlighted machine on the map.' : (game.cardDraft?.pending ? 'Choose upgrade card before building.' : TEXT.status.build(game.buildTool)))
|
||||
: TEXT.status.sorting;
|
||||
}
|
||||
|
||||
|
|
@ -55,12 +56,14 @@ export function createUISystem({ game, ui, build, startGame, activeQueuedChick }
|
|||
function updateTurnSummary() {
|
||||
const connected = game.eggFarms.filter(f => routeFromFarmToScanner(game, f)).length;
|
||||
const issues = facilityConnectionIssues(game);
|
||||
const tax = zundaTaxInfo(game.cash);
|
||||
const tax = zundaTaxInfo(game.cash, game);
|
||||
const tribute = fairiesTributeInfo(game, game.turn);
|
||||
ui.turnSummary.innerHTML = [
|
||||
`Truck target: <b>${truckTarget(game).toUpperCase()}</b> | Male fine: <b>-${yen(maleTruckPenalty(game))}</b>`,
|
||||
`Poop fine: <b>Mixer -${yen(mixerPoopPenalty(game))}</b> / <b>Shipment -${yen(truckPoopPenalty(game))}</b>`,
|
||||
`ZUNDA TAX on Next Day: <b>-${yen(tax.tax)}</b> | taxable ${yen(tax.taxable)} × ${tax.ratePercent}%`,
|
||||
`Belt: <b>${displaySpeed()}px/s fixed</b> | Farms connected: <b>${connected}/${game.eggFarms.length}</b>`,
|
||||
`ZUNDA TAX on Next Day: <b>-${yen(tax.tax)}</b> | taxable ${yen(tax.taxable)} × ${tax.ratePercent}% | exemption ${yen(tax.exemption)} | 95% at ${yen(tax.maxRateCash)}`,
|
||||
`Fairies tribute on Next Day: <b>-${yen(tribute.amount)}</b>${tribute.reduction ? ` | flattery -${yen(tribute.reduction)}` : ''}`,
|
||||
`Card draft: <b>${game.cardDraft?.pending ? Math.max(1, game.cardDraft.picksRemaining || 1) + ' pick(s) left' : 'ready after each day'}</b> | Belt: <b>${displaySpeed()}px/s</b> | Farms connected: <b>${connected}/${game.eggFarms.length}</b>`,
|
||||
issues.length ? `<span class="cash-negative">Blocked: ${issues[0]}</span>` : `<span class="cash-positive">${TEXT.status.allPortsConnected}</span>`
|
||||
].join('<br>');
|
||||
}
|
||||
|
|
@ -104,7 +107,7 @@ export function createUISystem({ game, ui, build, startGame, activeQueuedChick }
|
|||
ui.facilityPanel.className = 'facility-card';
|
||||
const lines = build.selectedInfoLines(obj);
|
||||
if (obj.type === 'facility' && obj.id === 'trash') lines.push('Items are shredded; no stored contents are visualized.');
|
||||
ui.facilityPanel.innerHTML = `<h3>${build.selectedTitle(obj)}</h3>${lines.map(x => `<p>${x}</p>`).join('')}<p><b>Upgrade choices appear in the clicked equipment menu.</b></p>`;
|
||||
ui.facilityPanel.innerHTML = `<h3>${build.selectedTitle(obj)}</h3>${lines.map(x => `<p>${x}</p>`).join('')}<p><b>Upgrades come from pre-Build card choices.</b></p>`;
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
|
|
@ -134,9 +137,11 @@ export function createUISystem({ game, ui, build, startGame, activeQueuedChick }
|
|||
ui.buttons.s1Right.disabled = game.phase !== 'running' || !activeQueuedChick(0);
|
||||
ui.buttons.s2Left.disabled = game.phase !== 'running' || !activeQueuedChick(1);
|
||||
ui.buttons.s2Right.disabled = game.phase !== 'running' || !activeQueuedChick(1);
|
||||
const nextTax = zundaTaxInfo(game.cash).tax;
|
||||
ui.buttons.nextTurn.textContent = game.phase === 'build' && nextTax > 0 ? `Next Day - ZUNDA TAX ${yen(nextTax)}` : TEXT.actions.nextDay;
|
||||
ui.buttons.nextTurn.disabled = game.phase !== 'build' || !factoryReady(game);
|
||||
const nextTax = zundaTaxInfo(game.cash, game).tax;
|
||||
const nextTribute = fairiesTributeInfo(game, game.turn).amount;
|
||||
const nextFees = nextTax + nextTribute;
|
||||
ui.buttons.nextTurn.textContent = game.phase === 'build' && nextFees > 0 ? `Next Day - ZUNDA ${yen(nextTax)} / Fairies ${yen(nextTribute)}` : TEXT.actions.nextDay;
|
||||
ui.buttons.nextTurn.disabled = game.phase !== 'build' || !!game.cardDraft?.pending || !!game.cardTargetPick?.pending || !factoryReady(game);
|
||||
}
|
||||
|
||||
function updatePriorityStrip() {
|
||||
|
|
@ -144,7 +149,7 @@ export function createUISystem({ game, ui, build, startGame, activeQueuedChick }
|
|||
const target = truckTarget(game).toUpperCase();
|
||||
ui.targetBrief.textContent = `TRUCK TARGET: ${target}`;
|
||||
ui.speedBrief.textContent = `BELT: ${displaySpeed()} px/s`;
|
||||
ui.speedBrief.title = `Conveyor speed is fixed by day. Cap: ${CONVEYOR_SPEED_MAX}px/s.`;
|
||||
ui.speedBrief.title = `Conveyor speed does not increase by day. Cap: ${CONVEYOR_SPEED_MAX}px/s.`;
|
||||
const active = game.contractActive;
|
||||
const offer = game.contractOffer;
|
||||
if (active) {
|
||||
|
|
@ -199,7 +204,7 @@ export function createUISystem({ game, ui, build, startGame, activeQueuedChick }
|
|||
<div><strong>Mixer income</strong><span>${yen(r.mixerRevenue)} / unit ${yen(upgradedMixerPrice(game))}</span></div>
|
||||
<div><strong>Poop fine</strong><span>Mixer ${yen(r.mixerPoopFine)} / Shipment ${yen(r.truckPoopFine)}</span></div>
|
||||
<div><strong>Wrong truck fine</strong><span>${yen(r.maleTruckFine)}</span></div>
|
||||
<div><strong>ZUNDA TAX</strong><span>Opening tax paid on Next Day: -${yen(r.zundaTax || 0)}</span></div>
|
||||
<div><strong>Next Day fees</strong><span>ZUNDA TAX and Fairies tribute are paid before the next run and are not included in this day's profit.</span></div>
|
||||
<div><strong>Explosion penalty</strong><span>${yen(r.explosionDamage)}</span></div>
|
||||
<div><strong>Daily earned formula</strong><span>(${yen(r.mixerRevenue)} + ${yen(r.ship.adjusted)} + ${yen(contractBonus)}) ÷ 1 day = ${yen(dailyEarned)}</span></div>
|
||||
<div><strong>Net profit formula</strong><span>${yen(r.revenue)} - ${yen(r.penalty)} = ${yen(r.profit)}</span></div>
|
||||
|
|
@ -207,7 +212,7 @@ export function createUISystem({ game, ui, build, startGame, activeQueuedChick }
|
|||
</div>
|
||||
${contractHtml}`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
ui.modalActions.appendChild(button(TEXT.actions.buildPhase, hideModal, 'primary-button'));
|
||||
ui.modalActions.appendChild(button('Choose Upgrade Card', beginCardDraft || hideModal, 'primary-button'));
|
||||
ui.modal.classList.remove('equipment-popover');
|
||||
ui.modal.classList.add('visible');
|
||||
}
|
||||
|
|
@ -219,7 +224,7 @@ export function createUISystem({ game, ui, build, startGame, activeQueuedChick }
|
|||
ui.modalTitle.textContent = TEXT.phases.gameover;
|
||||
ui.modalBody.innerHTML = `<p>Your cash went negative.</p>
|
||||
<div class="formula-box"><strong>Final score formula</strong><br>Base = Cash + Total revenue + Factory value × 0.5 + Contract bonus × 0.5 = ${yen(fs.base)}<br>Daily earned = ceil(Base ÷ Days) = ceil(${yen(fs.base)} ÷ ${fs.days}) = ${yen(fs.dailyEarned)}<br>Score = max(0, floor(Base + Daily earned + Correct×5 - Total outflow - Day penalty)) = ${fs.score}</div>
|
||||
<div class="result-grid"><div><strong>Final Score</strong><span>${fs.score}</span></div><div><strong>Survival</strong><span>${game.totals.turnsCompleted} days</span></div><div><strong>Final Cash</strong><span>${yen(game.cash)}</span></div><div><strong>Factory Value</strong><span>${yen(fs.factoryValue)}</span></div><div><strong>Total Revenue</strong><span>${yen(game.totals.revenue)}</span></div><div><strong>Daily Earned</strong><span>${yen(fs.dailyEarned)} / day</span></div><div><strong>Total Outflow</strong><span>${yen(game.totals.penalty)}</span></div><div><strong>ZUNDA TAX</strong><span>${yen(game.totals.zundaTax)}</span></div><div><strong>Correct Sorts</strong><span>${game.totals.correct}</span></div><div><strong>Accuracy</strong><span>${accuracy}%</span></div><div><strong>Auto Sorted</strong><span>${game.totals.autoSorted}</span></div><div><strong>Poop Control</strong><span>${game.totals.poopTrash} trashed / ${game.totals.poopTruck} shipped / ${game.totals.poopMixer} mixer</span></div><div><strong>Explosion Penalty</strong><span>${yen(game.totals.explosionDamage)}</span></div><div><strong>Contract Bonus</strong><span>${yen(game.totals.contractBonus)}</span></div><div><strong>Day Penalty</strong><span>-${fs.dayPenalty}</span></div><div><strong>Contracts</strong><span>${game.totals.contractSuccess} success / ${game.totals.contractFailed} failed</span></div></div>`;
|
||||
<div class="result-grid"><div><strong>Final Score</strong><span>${fs.score}</span></div><div><strong>Survival</strong><span>${game.totals.turnsCompleted} days</span></div><div><strong>Final Cash</strong><span>${yen(game.cash)}</span></div><div><strong>Factory Value</strong><span>${yen(fs.factoryValue)}</span></div><div><strong>Total Revenue</strong><span>${yen(game.totals.revenue)}</span></div><div><strong>Daily Earned</strong><span>${yen(fs.dailyEarned)} / day</span></div><div><strong>Total Outflow</strong><span>${yen(game.totals.penalty)}</span></div><div><strong>ZUNDA TAX</strong><span>${yen(game.totals.zundaTax)}</span></div><div><strong>Fairies Tribute</strong><span>${yen(game.totals.fairiesTribute || 0)}</span></div><div><strong>Correct Sorts</strong><span>${game.totals.correct}</span></div><div><strong>Accuracy</strong><span>${accuracy}%</span></div><div><strong>Auto Sorted</strong><span>${game.totals.autoSorted}</span></div><div><strong>Poop Control</strong><span>${game.totals.poopTrash} trashed / ${game.totals.poopTruck} shipped / ${game.totals.poopMixer} mixer</span></div><div><strong>Explosion Penalty</strong><span>${yen(game.totals.explosionDamage)}</span></div><div><strong>Contract Bonus</strong><span>${yen(game.totals.contractBonus)}</span></div><div><strong>Day Penalty</strong><span>-${fs.dayPenalty}</span></div><div><strong>Contracts</strong><span>${game.totals.contractSuccess} success / ${game.totals.contractFailed} failed</span></div></div>`;
|
||||
ui.modalActions.innerHTML = '';
|
||||
ui.modalActions.appendChild(button(TEXT.actions.restart, startGame, 'primary-button'));
|
||||
ui.modal.classList.remove('equipment-popover');
|
||||
|
|
|
|||
28
styles.css
28
styles.css
|
|
@ -180,3 +180,31 @@ h1, h2, p { margin: 0; }
|
|||
margin: 6px 0;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
/* v15 pre-build card draft */
|
||||
.card-choices {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.card-choice {
|
||||
min-height: 132px;
|
||||
border: 3px solid var(--line);
|
||||
background: #f7fff5;
|
||||
color: var(--ink);
|
||||
box-shadow: 5px 5px 0 rgba(16,32,21,.16);
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-weight: 900;
|
||||
}
|
||||
.card-choice:hover { transform: translate(-1px, -1px); box-shadow: 6px 6px 0 rgba(16,32,21,.18); }
|
||||
.card-choice strong { display: block; font-size: 14px; text-transform: uppercase; line-height: 1.2; }
|
||||
.card-choice span { display: inline-block; margin: 8px 0; padding: 2px 7px; border: 2px solid var(--line); background: var(--white); font-size: 9px; letter-spacing: .1em; }
|
||||
.card-choice small { display: block; color: var(--muted); font-size: 11px; line-height: 1.35; }
|
||||
.card-choice.rare { background: #fff0d1; box-shadow: inset 0 0 0 3px #f0a020, 5px 5px 0 rgba(16,32,21,.16); }
|
||||
.muted-card-note { color: var(--muted); font-weight: 700; }
|
||||
.card-targets { display: grid; gap: 8px; margin-top: 14px; }
|
||||
.card-target-button { border: 3px solid var(--line); background: #f7fff5; padding: 10px 12px; text-align: left; cursor: pointer; font-weight: 900; box-shadow: 4px 4px 0 rgba(16,32,21,.14); }
|
||||
.card-target-button:hover { background: var(--green-soft); }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue