feat: Simplification of UI and icons creation
This commit is contained in:
@@ -0,0 +1,586 @@
|
||||
ICONS TO PRODUCE
|
||||
================
|
||||
|
||||
Generated by "npm run icons -- --write" from the game data: do not edit by hand.
|
||||
93 icons per theme, 465 in total (5 themes: paperclip, witch, alien, insects, computers).
|
||||
|
||||
Format One square image per icon: SVG preferred (or PNG, 128x128 px or more),
|
||||
transparent background, readable at 28 px (top bar) and 48 px (lists).
|
||||
Files public/themes/<theme>/img/<file>, then listed in that theme's theme.json:
|
||||
{ "assets": { "<key>": "img/<file>" } } (an icon that is not listed keeps its grey square)
|
||||
|
||||
SUMMARY (icons per theme)
|
||||
6 Resources [top bar (units, funds, material), Gamble, Research and Legacy screens]
|
||||
6 Automation tiers [Production screen, Automation list]
|
||||
36 Upgrades [Production screen, Upgrades list]
|
||||
14 Endless upgrades [Production screen, Endless upgrades panel]
|
||||
9 Investment societies [Investments screen]
|
||||
15 Research [Research screen]
|
||||
6 Legacy perks [Legacy screen]
|
||||
1 Trophy [Achievements screen (one shared icon for all achievements)]
|
||||
93 TOTAL
|
||||
|
||||
|
||||
THEME: paperclip
|
||||
----------------
|
||||
|
||||
Resources (6)
|
||||
[ ] img/res-units.svg res.units Paperclips
|
||||
[ ] img/res-funds.svg res.funds Funds
|
||||
[ ] img/res-material.svg res.material Wire
|
||||
[ ] img/res-chips.svg res.chips Quantum Chips
|
||||
[ ] img/res-research.svg res.research R&D Points
|
||||
[ ] img/res-legacy.svg res.legacy Patents
|
||||
|
||||
Automation tiers (6)
|
||||
[ ] img/gen-gen1.svg gen.gen1 Autoclipper
|
||||
[ ] img/gen-gen2.svg gen.gen2 MegaClipper
|
||||
[ ] img/gen-gen3.svg gen.gen3 Clip Factory
|
||||
[ ] img/gen-gen4.svg gen.gen4 Hypno Drones
|
||||
[ ] img/gen-gen5.svg gen.gen5 Probe Swarm
|
||||
[ ] img/gen-gen6.svg gen.gen6 Matter Converter
|
||||
|
||||
Upgrades (36)
|
||||
[ ] img/upg-click_1.svg upg.click_1 Ergonomic Bender
|
||||
[ ] img/upg-click_2.svg upg.click_2 Pneumatic Grip
|
||||
[ ] img/upg-click_3.svg upg.click_3 Robotic Arm
|
||||
[ ] img/upg-g1_a.svg upg.g1_a Sharper Autoclippers
|
||||
[ ] img/upg-g1_b.svg upg.g1_b Overclocked Autoclippers
|
||||
[ ] img/upg-g2_a.svg upg.g2_a MegaClipper Servos
|
||||
[ ] img/upg-g2_b.svg upg.g2_b MegaClipper Turbines
|
||||
[ ] img/upg-g3_a.svg upg.g3_a Assembly Line
|
||||
[ ] img/upg-g3_b.svg upg.g3_b Just-in-Time Logistics
|
||||
[ ] img/upg-g4_a.svg upg.g4_a Hypno Harmonics
|
||||
[ ] img/upg-g4_b.svg upg.g4_b Persuasion Algorithms
|
||||
[ ] img/upg-g5_a.svg upg.g5_a Probe Self-Replication
|
||||
[ ] img/upg-g5_b.svg upg.g5_b Von Neumann Protocols
|
||||
[ ] img/upg-g6_a.svg upg.g6_a Atomic Reassembly
|
||||
[ ] img/upg-g6_b.svg upg.g6_b Universal Reformatting
|
||||
[ ] img/upg-global_1.svg upg.global_1 Quality Control
|
||||
[ ] img/upg-global_2.svg upg.global_2 Lean Manufacturing
|
||||
[ ] img/upg-global_3.svg upg.global_3 Industrial AI
|
||||
[ ] img/upg-global_4.svg upg.global_4 Quantum Scheduling
|
||||
[ ] img/upg-demand_1.svg upg.demand_1 Newspaper Ad
|
||||
[ ] img/upg-demand_2.svg upg.demand_2 Radio Campaign
|
||||
[ ] img/upg-demand_3.svg upg.demand_3 Viral Video
|
||||
[ ] img/upg-demand_4.svg upg.demand_4 Global Brand
|
||||
[ ] img/upg-value_1.svg upg.value_1 Gold-Plated Paperclips
|
||||
[ ] img/upg-value_2.svg upg.value_2 Premium Packaging
|
||||
[ ] img/upg-value_3.svg upg.value_3 Luxury Branding
|
||||
[ ] img/upg-mat_eff_1.svg upg.mat_eff_1 Thinner Wire
|
||||
[ ] img/upg-mat_eff_2.svg upg.mat_eff_2 Wire Recycling
|
||||
[ ] img/upg-mat_disc_1.svg upg.mat_disc_1 Bulk Wire Contract
|
||||
[ ] img/upg-mat_disc_2.svg upg.mat_disc_2 Direct From the Mill
|
||||
[ ] img/upg-stack_1.svg upg.stack_1 Bigger Spools
|
||||
[ ] img/upg-stack_2.svg upg.stack_2 Pallet Orders
|
||||
[ ] img/upg-stack_3.svg upg.stack_3 Container Loads
|
||||
[ ] img/upg-stack_4.svg upg.stack_4 Freight Trains
|
||||
[ ] img/upg-stack_5.svg upg.stack_5 Wire Mills
|
||||
[ ] img/upg-auto_buy.svg upg.auto_buy WireBuyer
|
||||
|
||||
Endless upgrades (14)
|
||||
[ ] img/endless-e_click.svg endless.e_click Manual mastery
|
||||
[ ] img/endless-e_gen1.svg endless.e_gen1 Autoclipper overhaul
|
||||
[ ] img/endless-e_gen2.svg endless.e_gen2 MegaClipper overhaul
|
||||
[ ] img/endless-e_gen3.svg endless.e_gen3 Clip Factory overhaul
|
||||
[ ] img/endless-e_gen4.svg endless.e_gen4 Hypno Drones overhaul
|
||||
[ ] img/endless-e_gen5.svg endless.e_gen5 Probe Swarm overhaul
|
||||
[ ] img/endless-e_gen6.svg endless.e_gen6 Matter Converter overhaul
|
||||
[ ] img/endless-e_global.svg endless.e_global Total optimisation
|
||||
[ ] img/endless-e_demand.svg endless.e_demand Mass appeal
|
||||
[ ] img/endless-e_value.svg endless.e_value Premium margin
|
||||
[ ] img/endless-e_efficiency.svg endless.e_efficiency Wire thrift
|
||||
[ ] img/endless-e_discount.svg endless.e_discount Hard bargaining
|
||||
[ ] img/endless-e_stack.svg endless.e_stack Wire stockpiling
|
||||
[ ] img/endless-e_cost.svg endless.e_cost Economies of scale
|
||||
|
||||
Investment societies (9)
|
||||
[ ] img/inv-inv_low_1.svg inv.inv_low_1 Steady Staples Inc.
|
||||
[ ] img/inv-inv_low_2.svg inv.inv_low_2 Office Supply Trust
|
||||
[ ] img/inv-inv_low_3.svg inv.inv_low_3 Copper Wire Bond
|
||||
[ ] img/inv-inv_mid_1.svg inv.inv_mid_1 Binder Dynamics
|
||||
[ ] img/inv-inv_mid_2.svg inv.inv_mid_2 Clip Logistics
|
||||
[ ] img/inv-inv_mid_3.svg inv.inv_mid_3 Stationery Futures
|
||||
[ ] img/inv-inv_high_1.svg inv.inv_high_1 NanoWire Labs
|
||||
[ ] img/inv-inv_high_2.svg inv.inv_high_2 Paperless Dreams Ltd
|
||||
[ ] img/inv-inv_high_3.svg inv.inv_high_3 Hyper-Clip Ventures
|
||||
|
||||
Research (15)
|
||||
[ ] img/research-r_prod_1.svg research.r_prod_1 Optimised Workflows
|
||||
[ ] img/research-r_prod_2.svg research.r_prod_2 Automation Suite
|
||||
[ ] img/research-r_prod_3.svg research.r_prod_3 Total Integration
|
||||
[ ] img/research-r_mkt_1.svg research.r_mkt_1 Market Analysis
|
||||
[ ] img/research-r_mkt_2.svg research.r_mkt_2 Brand Strategy
|
||||
[ ] img/research-r_mkt_3.svg research.r_mkt_3 Premium Positioning
|
||||
[ ] img/research-r_log_1.svg research.r_log_1 Resource Efficiency
|
||||
[ ] img/research-r_log_2.svg research.r_log_2 Bulk Procurement
|
||||
[ ] img/research-r_log_3.svg research.r_log_3 Lean Manufacturing
|
||||
[ ] img/research-r_fin_1.svg research.r_fin_1 Portfolio Management
|
||||
[ ] img/research-r_fin_2.svg research.r_fin_2 Risk Modelling
|
||||
[ ] img/research-r_fin_3.svg research.r_fin_3 Negotiation Training
|
||||
[ ] img/research-r_ins_1.svg research.r_ins_1 Research Methods
|
||||
[ ] img/research-r_ins_2.svg research.r_ins_2 Remote Operations
|
||||
[ ] img/research-r_ins_3.svg research.r_ins_3 Price Oracle
|
||||
|
||||
Legacy perks (6)
|
||||
[ ] img/legacy-lg_prod.svg legacy.lg_prod Industrial Memory
|
||||
[ ] img/legacy-lg_demand.svg legacy.lg_demand Brand Heritage
|
||||
[ ] img/legacy-lg_cost.svg legacy.lg_cost Lean Blueprints
|
||||
[ ] img/legacy-lg_start.svg legacy.lg_start Head Start
|
||||
[ ] img/legacy-lg_offline.svg legacy.lg_offline Night Shift
|
||||
[ ] img/legacy-lg_rp.svg legacy.lg_rp Research Archive
|
||||
|
||||
Trophy (1)
|
||||
[ ] img/trophy.svg trophy A trophy
|
||||
|
||||
|
||||
THEME: witch
|
||||
------------
|
||||
|
||||
Resources (6)
|
||||
[ ] img/res-units.svg res.units Potions
|
||||
[ ] img/res-funds.svg res.funds Gold
|
||||
[ ] img/res-material.svg res.material Herbs
|
||||
[ ] img/res-chips.svg res.chips Fate Tokens
|
||||
[ ] img/res-research.svg res.research Arcane Insight
|
||||
[ ] img/res-legacy.svg res.legacy Grimoire Pages
|
||||
|
||||
Automation tiers (6)
|
||||
[ ] img/gen-gen1.svg gen.gen1 Apprentice
|
||||
[ ] img/gen-gen2.svg gen.gen2 Bubbling Cauldron
|
||||
[ ] img/gen-gen3.svg gen.gen3 Alchemy Lab
|
||||
[ ] img/gen-gen4.svg gen.gen4 Broomstick Fleet
|
||||
[ ] img/gen-gen5.svg gen.gen5 Moonlit Coven
|
||||
[ ] img/gen-gen6.svg gen.gen6 Philosopher's Engine
|
||||
|
||||
Upgrades (36)
|
||||
[ ] img/upg-click_1.svg upg.click_1 Steady Stirring
|
||||
[ ] img/upg-click_2.svg upg.click_2 Enchanted Ladle
|
||||
[ ] img/upg-click_3.svg upg.click_3 Familiar's Help
|
||||
[ ] img/upg-g1_a.svg upg.g1_a Apprentice Lessons
|
||||
[ ] img/upg-g1_b.svg upg.g1_b Apprentice Mastery
|
||||
[ ] img/upg-g2_a.svg upg.g2_a Copper Cauldrons
|
||||
[ ] img/upg-g2_b.svg upg.g2_b Eternal Flame
|
||||
[ ] img/upg-g3_a.svg upg.g3_a Crystal Alembics
|
||||
[ ] img/upg-g3_b.svg upg.g3_b Rune-Etched Tables
|
||||
[ ] img/upg-g4_a.svg upg.g4_a Waxed Brooms
|
||||
[ ] img/upg-g4_b.svg upg.g4_b Hex-Powered Flight
|
||||
[ ] img/upg-g5_a.svg upg.g5_a Full Moon Rites
|
||||
[ ] img/upg-g5_b.svg upg.g5_b Blood Moon Pact
|
||||
[ ] img/upg-g6_a.svg upg.g6_a Transmutation Circles
|
||||
[ ] img/upg-g6_b.svg upg.g6_b The Fifth Element
|
||||
[ ] img/upg-global_1.svg upg.global_1 Spellbook Index
|
||||
[ ] img/upg-global_2.svg upg.global_2 Grandmother's Grimoire
|
||||
[ ] img/upg-global_3.svg upg.global_3 Ley Line Tap
|
||||
[ ] img/upg-global_4.svg upg.global_4 Time Dilation Charm
|
||||
[ ] img/upg-demand_1.svg upg.demand_1 Village Notice Board
|
||||
[ ] img/upg-demand_2.svg upg.demand_2 Market Stall
|
||||
[ ] img/upg-demand_3.svg upg.demand_3 Royal Commission
|
||||
[ ] img/upg-demand_4.svg upg.demand_4 Legend of the Witch
|
||||
[ ] img/upg-value_1.svg upg.value_1 Glittering Labels
|
||||
[ ] img/upg-value_2.svg upg.value_2 Apothecary Seal
|
||||
[ ] img/upg-value_3.svg upg.value_3 Royal Warrant
|
||||
[ ] img/upg-mat_eff_1.svg upg.mat_eff_1 Careful Pruning
|
||||
[ ] img/upg-mat_eff_2.svg upg.mat_eff_2 Distilled Essence
|
||||
[ ] img/upg-mat_disc_1.svg upg.mat_disc_1 Forager's Pact
|
||||
[ ] img/upg-mat_disc_2.svg upg.mat_disc_2 Enchanted Garden
|
||||
[ ] img/upg-stack_1.svg upg.stack_1 Larger Baskets
|
||||
[ ] img/upg-stack_2.svg upg.stack_2 Herb Sacks
|
||||
[ ] img/upg-stack_3.svg upg.stack_3 Cartloads
|
||||
[ ] img/upg-stack_4.svg upg.stack_4 Greenhouse Harvest
|
||||
[ ] img/upg-stack_5.svg upg.stack_5 Whole-Forest Rights
|
||||
[ ] img/upg-auto_buy.svg upg.auto_buy Self-Gathering Basket
|
||||
|
||||
Endless upgrades (14)
|
||||
[ ] img/endless-e_click.svg endless.e_click Manual mastery
|
||||
[ ] img/endless-e_gen1.svg endless.e_gen1 Apprentice overhaul
|
||||
[ ] img/endless-e_gen2.svg endless.e_gen2 Bubbling Cauldron overhaul
|
||||
[ ] img/endless-e_gen3.svg endless.e_gen3 Alchemy Lab overhaul
|
||||
[ ] img/endless-e_gen4.svg endless.e_gen4 Broomstick Fleet overhaul
|
||||
[ ] img/endless-e_gen5.svg endless.e_gen5 Moonlit Coven overhaul
|
||||
[ ] img/endless-e_gen6.svg endless.e_gen6 Philosopher's Engine overhaul
|
||||
[ ] img/endless-e_global.svg endless.e_global Total optimisation
|
||||
[ ] img/endless-e_demand.svg endless.e_demand Mass appeal
|
||||
[ ] img/endless-e_value.svg endless.e_value Premium margin
|
||||
[ ] img/endless-e_efficiency.svg endless.e_efficiency Herbs thrift
|
||||
[ ] img/endless-e_discount.svg endless.e_discount Hard bargaining
|
||||
[ ] img/endless-e_stack.svg endless.e_stack Herbs stockpiling
|
||||
[ ] img/endless-e_cost.svg endless.e_cost Economies of scale
|
||||
|
||||
Investment societies (9)
|
||||
[ ] img/inv-inv_low_1.svg inv.inv_low_1 Old Hollow Apothecary
|
||||
[ ] img/inv-inv_low_2.svg inv.inv_low_2 Greywood Herb Co-op
|
||||
[ ] img/inv-inv_low_3.svg inv.inv_low_3 Village Cauldron Guild
|
||||
[ ] img/inv-inv_mid_1.svg inv.inv_mid_1 Moonpetal Brewers
|
||||
[ ] img/inv-inv_mid_2.svg inv.inv_mid_2 Nightshade & Sons
|
||||
[ ] img/inv-inv_mid_3.svg inv.inv_mid_3 Crooked Broom Trading
|
||||
[ ] img/inv-inv_high_1.svg inv.inv_high_1 Dragonscale Elixirs
|
||||
[ ] img/inv-inv_high_2.svg inv.inv_high_2 Wyrmwood Potions Ltd
|
||||
[ ] img/inv-inv_high_3.svg inv.inv_high_3 Forbidden Brew Cartel
|
||||
|
||||
Research (15)
|
||||
[ ] img/research-r_prod_1.svg research.r_prod_1 Optimised Workflows
|
||||
[ ] img/research-r_prod_2.svg research.r_prod_2 Automation Suite
|
||||
[ ] img/research-r_prod_3.svg research.r_prod_3 Total Integration
|
||||
[ ] img/research-r_mkt_1.svg research.r_mkt_1 Market Analysis
|
||||
[ ] img/research-r_mkt_2.svg research.r_mkt_2 Brand Strategy
|
||||
[ ] img/research-r_mkt_3.svg research.r_mkt_3 Premium Positioning
|
||||
[ ] img/research-r_log_1.svg research.r_log_1 Resource Efficiency
|
||||
[ ] img/research-r_log_2.svg research.r_log_2 Bulk Procurement
|
||||
[ ] img/research-r_log_3.svg research.r_log_3 Lean Manufacturing
|
||||
[ ] img/research-r_fin_1.svg research.r_fin_1 Portfolio Management
|
||||
[ ] img/research-r_fin_2.svg research.r_fin_2 Risk Modelling
|
||||
[ ] img/research-r_fin_3.svg research.r_fin_3 Negotiation Training
|
||||
[ ] img/research-r_ins_1.svg research.r_ins_1 Research Methods
|
||||
[ ] img/research-r_ins_2.svg research.r_ins_2 Remote Operations
|
||||
[ ] img/research-r_ins_3.svg research.r_ins_3 Price Oracle
|
||||
|
||||
Legacy perks (6)
|
||||
[ ] img/legacy-lg_prod.svg legacy.lg_prod Industrial Memory
|
||||
[ ] img/legacy-lg_demand.svg legacy.lg_demand Brand Heritage
|
||||
[ ] img/legacy-lg_cost.svg legacy.lg_cost Lean Blueprints
|
||||
[ ] img/legacy-lg_start.svg legacy.lg_start Head Start
|
||||
[ ] img/legacy-lg_offline.svg legacy.lg_offline Night Shift
|
||||
[ ] img/legacy-lg_rp.svg legacy.lg_rp Research Archive
|
||||
|
||||
Trophy (1)
|
||||
[ ] img/trophy.svg trophy A trophy
|
||||
|
||||
|
||||
THEME: alien
|
||||
------------
|
||||
|
||||
Resources (6)
|
||||
[ ] img/res-units.svg res.units Xenocrystals
|
||||
[ ] img/res-funds.svg res.funds Credits
|
||||
[ ] img/res-material.svg res.material Raw ore
|
||||
[ ] img/res-chips.svg res.chips Chance Crystals
|
||||
[ ] img/res-research.svg res.research Xeno-Data
|
||||
[ ] img/res-legacy.svg res.legacy Blueprints
|
||||
|
||||
Automation tiers (6)
|
||||
[ ] img/gen-gen1.svg gen.gen1 Mining Drone
|
||||
[ ] img/gen-gen2.svg gen.gen2 Plasma Drill
|
||||
[ ] img/gen-gen3.svg gen.gen3 Orbital Extractor
|
||||
[ ] img/gen-gen4.svg gen.gen4 Asteroid Harvester
|
||||
[ ] img/gen-gen5.svg gen.gen5 Hive Swarm
|
||||
[ ] img/gen-gen6.svg gen.gen6 Planet Cracker
|
||||
|
||||
Upgrades (36)
|
||||
[ ] img/upg-click_1.svg upg.click_1 Gravity Gloves
|
||||
[ ] img/upg-click_2.svg upg.click_2 Exo-Suit
|
||||
[ ] img/upg-click_3.svg upg.click_3 Neural Link
|
||||
[ ] img/upg-g1_a.svg upg.g1_a Drone Firmware
|
||||
[ ] img/upg-g1_b.svg upg.g1_b Drone Swarm Logic
|
||||
[ ] img/upg-g2_a.svg upg.g2_a Diamond Bits
|
||||
[ ] img/upg-g2_b.svg upg.g2_b Plasma Focusing
|
||||
[ ] img/upg-g3_a.svg upg.g3_a Tether Cables
|
||||
[ ] img/upg-g3_b.svg upg.g3_b Zero-G Smelting
|
||||
[ ] img/upg-g4_a.svg upg.g4_a Mass Drivers
|
||||
[ ] img/upg-g4_b.svg upg.g4_b Autonomous Prospecting
|
||||
[ ] img/upg-g5_a.svg upg.g5_a Hive Mind Sync
|
||||
[ ] img/upg-g5_b.svg upg.g5_b Queen Directive
|
||||
[ ] img/upg-g6_a.svg upg.g6_a Tectonic Resonance
|
||||
[ ] img/upg-g6_b.svg upg.g6_b Core Shattering
|
||||
[ ] img/upg-global_1.svg upg.global_1 Quantum Diagnostics
|
||||
[ ] img/upg-global_2.svg upg.global_2 Alien Alloys
|
||||
[ ] img/upg-global_3.svg upg.global_3 Dyson Tap
|
||||
[ ] img/upg-global_4.svg upg.global_4 Spacetime Folding
|
||||
[ ] img/upg-demand_1.svg upg.demand_1 Trade Beacon
|
||||
[ ] img/upg-demand_2.svg upg.demand_2 Interstellar Broker
|
||||
[ ] img/upg-demand_3.svg upg.demand_3 Galactic Exchange
|
||||
[ ] img/upg-demand_4.svg upg.demand_4 Cosmic Monopoly
|
||||
[ ] img/upg-value_1.svg upg.value_1 Purity Certificate
|
||||
[ ] img/upg-value_2.svg upg.value_2 Rare Spectrum Cut
|
||||
[ ] img/upg-value_3.svg upg.value_3 Imperial Grade
|
||||
[ ] img/upg-mat_eff_1.svg upg.mat_eff_1 Ore Analyzer
|
||||
[ ] img/upg-mat_eff_2.svg upg.mat_eff_2 Molecular Sieve
|
||||
[ ] img/upg-mat_disc_1.svg upg.mat_disc_1 Asteroid Contract
|
||||
[ ] img/upg-mat_disc_2.svg upg.mat_disc_2 Own the Mine
|
||||
[ ] img/upg-stack_1.svg upg.stack_1 Bigger Cargo Pods
|
||||
[ ] img/upg-stack_2.svg upg.stack_2 Container Drones
|
||||
[ ] img/upg-stack_3.svg upg.stack_3 Freighter Holds
|
||||
[ ] img/upg-stack_4.svg upg.stack_4 Convoy Fleets
|
||||
[ ] img/upg-stack_5.svg upg.stack_5 Orbital Stockpile
|
||||
[ ] img/upg-auto_buy.svg upg.auto_buy Autonomous Freighter
|
||||
|
||||
Endless upgrades (14)
|
||||
[ ] img/endless-e_click.svg endless.e_click Manual mastery
|
||||
[ ] img/endless-e_gen1.svg endless.e_gen1 Mining Drone overhaul
|
||||
[ ] img/endless-e_gen2.svg endless.e_gen2 Plasma Drill overhaul
|
||||
[ ] img/endless-e_gen3.svg endless.e_gen3 Orbital Extractor overhaul
|
||||
[ ] img/endless-e_gen4.svg endless.e_gen4 Asteroid Harvester overhaul
|
||||
[ ] img/endless-e_gen5.svg endless.e_gen5 Hive Swarm overhaul
|
||||
[ ] img/endless-e_gen6.svg endless.e_gen6 Planet Cracker overhaul
|
||||
[ ] img/endless-e_global.svg endless.e_global Total optimisation
|
||||
[ ] img/endless-e_demand.svg endless.e_demand Mass appeal
|
||||
[ ] img/endless-e_value.svg endless.e_value Premium margin
|
||||
[ ] img/endless-e_efficiency.svg endless.e_efficiency Raw ore thrift
|
||||
[ ] img/endless-e_discount.svg endless.e_discount Hard bargaining
|
||||
[ ] img/endless-e_stack.svg endless.e_stack Raw ore stockpiling
|
||||
[ ] img/endless-e_cost.svg endless.e_cost Economies of scale
|
||||
|
||||
Investment societies (9)
|
||||
[ ] img/inv-inv_low_1.svg inv.inv_low_1 Zeta Mining Cooperative
|
||||
[ ] img/inv-inv_low_2.svg inv.inv_low_2 Orion Ore Trust
|
||||
[ ] img/inv-inv_low_3.svg inv.inv_low_3 Lunar Refinery Bond
|
||||
[ ] img/inv-inv_mid_1.svg inv.inv_mid_1 XenoCorp Industries
|
||||
[ ] img/inv-inv_mid_2.svg inv.inv_mid_2 Nebula Extraction Ltd
|
||||
[ ] img/inv-inv_mid_3.svg inv.inv_mid_3 Void Cargo Syndicate
|
||||
[ ] img/inv-inv_high_1.svg inv.inv_high_1 Hyperion Rift Labs
|
||||
[ ] img/inv-inv_high_2.svg inv.inv_high_2 Singularity Salvage
|
||||
[ ] img/inv-inv_high_3.svg inv.inv_high_3 Black Hole Ventures
|
||||
|
||||
Research (15)
|
||||
[ ] img/research-r_prod_1.svg research.r_prod_1 Optimised Workflows
|
||||
[ ] img/research-r_prod_2.svg research.r_prod_2 Automation Suite
|
||||
[ ] img/research-r_prod_3.svg research.r_prod_3 Total Integration
|
||||
[ ] img/research-r_mkt_1.svg research.r_mkt_1 Market Analysis
|
||||
[ ] img/research-r_mkt_2.svg research.r_mkt_2 Brand Strategy
|
||||
[ ] img/research-r_mkt_3.svg research.r_mkt_3 Premium Positioning
|
||||
[ ] img/research-r_log_1.svg research.r_log_1 Resource Efficiency
|
||||
[ ] img/research-r_log_2.svg research.r_log_2 Bulk Procurement
|
||||
[ ] img/research-r_log_3.svg research.r_log_3 Lean Manufacturing
|
||||
[ ] img/research-r_fin_1.svg research.r_fin_1 Portfolio Management
|
||||
[ ] img/research-r_fin_2.svg research.r_fin_2 Risk Modelling
|
||||
[ ] img/research-r_fin_3.svg research.r_fin_3 Negotiation Training
|
||||
[ ] img/research-r_ins_1.svg research.r_ins_1 Research Methods
|
||||
[ ] img/research-r_ins_2.svg research.r_ins_2 Remote Operations
|
||||
[ ] img/research-r_ins_3.svg research.r_ins_3 Price Oracle
|
||||
|
||||
Legacy perks (6)
|
||||
[ ] img/legacy-lg_prod.svg legacy.lg_prod Industrial Memory
|
||||
[ ] img/legacy-lg_demand.svg legacy.lg_demand Brand Heritage
|
||||
[ ] img/legacy-lg_cost.svg legacy.lg_cost Lean Blueprints
|
||||
[ ] img/legacy-lg_start.svg legacy.lg_start Head Start
|
||||
[ ] img/legacy-lg_offline.svg legacy.lg_offline Night Shift
|
||||
[ ] img/legacy-lg_rp.svg legacy.lg_rp Research Archive
|
||||
|
||||
Trophy (1)
|
||||
[ ] img/trophy.svg trophy A trophy
|
||||
|
||||
|
||||
THEME: insects
|
||||
--------------
|
||||
|
||||
Resources (6)
|
||||
[ ] img/res-units.svg res.units Leaves
|
||||
[ ] img/res-funds.svg res.funds Honeydew
|
||||
[ ] img/res-material.svg res.material Silk
|
||||
[ ] img/res-chips.svg res.chips Pollen Grains
|
||||
[ ] img/res-research.svg res.research Instinct
|
||||
[ ] img/res-legacy.svg res.legacy Colony Memories
|
||||
|
||||
Automation tiers (6)
|
||||
[ ] img/gen-gen1.svg gen.gen1 Worker Ant
|
||||
[ ] img/gen-gen2.svg gen.gen2 Leafcutter Squad
|
||||
[ ] img/gen-gen3.svg gen.gen3 Beetle Hauler
|
||||
[ ] img/gen-gen4.svg gen.gen4 Termite Mound
|
||||
[ ] img/gen-gen5.svg gen.gen5 Locust Swarm
|
||||
[ ] img/gen-gen6.svg gen.gen6 Hive Queen
|
||||
|
||||
Upgrades (36)
|
||||
[ ] img/upg-click_1.svg upg.click_1 Sharper Mandibles
|
||||
[ ] img/upg-click_2.svg upg.click_2 Pheromone Trail
|
||||
[ ] img/upg-click_3.svg upg.click_3 Double-Grip Legs
|
||||
[ ] img/upg-g1_a.svg upg.g1_a Ant Drill Sergeant
|
||||
[ ] img/upg-g1_b.svg upg.g1_b Marching Songs
|
||||
[ ] img/upg-g2_a.svg upg.g2_a Wider Leaf Slices
|
||||
[ ] img/upg-g2_b.svg upg.g2_b Leafcutter Guild
|
||||
[ ] img/upg-g3_a.svg upg.g3_a Reinforced Shells
|
||||
[ ] img/upg-g3_b.svg upg.g3_b Dung Beetle Logistics
|
||||
[ ] img/upg-g4_a.svg upg.g4_a Air-Conditioned Chambers
|
||||
[ ] img/upg-g4_b.svg upg.g4_b Deep Tunnel Network
|
||||
[ ] img/upg-g5_a.svg upg.g5_a Riding the Wind
|
||||
[ ] img/upg-g5_b.svg upg.g5_b Migration Instinct
|
||||
[ ] img/upg-g6_a.svg upg.g6_a Royal Jelly
|
||||
[ ] img/upg-g6_b.svg upg.g6_b Queen's Decree
|
||||
[ ] img/upg-global_1.svg upg.global_1 Dewdrop Hydration
|
||||
[ ] img/upg-global_2.svg upg.global_2 Seasonal Rhythm
|
||||
[ ] img/upg-global_3.svg upg.global_3 Hive Mind
|
||||
[ ] img/upg-global_4.svg upg.global_4 Perfect Spring
|
||||
[ ] img/upg-demand_1.svg upg.demand_1 Scent Markers
|
||||
[ ] img/upg-demand_2.svg upg.demand_2 Cricket Courier
|
||||
[ ] img/upg-demand_3.svg upg.demand_3 Forest Market
|
||||
[ ] img/upg-demand_4.svg upg.demand_4 Famous Pile
|
||||
[ ] img/upg-value_1.svg upg.value_1 Dewy Gloss
|
||||
[ ] img/upg-value_2.svg upg.value_2 Autumn Colours
|
||||
[ ] img/upg-value_3.svg upg.value_3 Golden Leaf Grade
|
||||
[ ] img/upg-mat_eff_1.svg upg.mat_eff_1 Tighter Weaving
|
||||
[ ] img/upg-mat_eff_2.svg upg.mat_eff_2 Silk Recycling
|
||||
[ ] img/upg-mat_disc_1.svg upg.mat_disc_1 Spider Contract
|
||||
[ ] img/upg-mat_disc_2.svg upg.mat_disc_2 Silkworm Farm
|
||||
[ ] img/upg-stack_1.svg upg.stack_1 Bigger Silk Bundles
|
||||
[ ] img/upg-stack_2.svg upg.stack_2 Cocoon Sacks
|
||||
[ ] img/upg-stack_3.svg upg.stack_3 Web Warehouses
|
||||
[ ] img/upg-stack_4.svg upg.stack_4 Spider Caravans
|
||||
[ ] img/upg-stack_5.svg upg.stack_5 Great Silk Hoard
|
||||
[ ] img/upg-auto_buy.svg upg.auto_buy Spider Delivery Service
|
||||
|
||||
Endless upgrades (14)
|
||||
[ ] img/endless-e_click.svg endless.e_click Manual mastery
|
||||
[ ] img/endless-e_gen1.svg endless.e_gen1 Worker Ant overhaul
|
||||
[ ] img/endless-e_gen2.svg endless.e_gen2 Leafcutter Squad overhaul
|
||||
[ ] img/endless-e_gen3.svg endless.e_gen3 Beetle Hauler overhaul
|
||||
[ ] img/endless-e_gen4.svg endless.e_gen4 Termite Mound overhaul
|
||||
[ ] img/endless-e_gen5.svg endless.e_gen5 Locust Swarm overhaul
|
||||
[ ] img/endless-e_gen6.svg endless.e_gen6 Hive Queen overhaul
|
||||
[ ] img/endless-e_global.svg endless.e_global Total optimisation
|
||||
[ ] img/endless-e_demand.svg endless.e_demand Mass appeal
|
||||
[ ] img/endless-e_value.svg endless.e_value Premium margin
|
||||
[ ] img/endless-e_efficiency.svg endless.e_efficiency Silk thrift
|
||||
[ ] img/endless-e_discount.svg endless.e_discount Hard bargaining
|
||||
[ ] img/endless-e_stack.svg endless.e_stack Silk stockpiling
|
||||
[ ] img/endless-e_cost.svg endless.e_cost Economies of scale
|
||||
|
||||
Investment societies (9)
|
||||
[ ] img/inv-inv_low_1.svg inv.inv_low_1 Anthill Savings Bank
|
||||
[ ] img/inv-inv_low_2.svg inv.inv_low_2 Honeycomb Trust
|
||||
[ ] img/inv-inv_low_3.svg inv.inv_low_3 Leafy Greens Co-op
|
||||
[ ] img/inv-inv_mid_1.svg inv.inv_mid_1 Dung Beetle Logistics Ltd
|
||||
[ ] img/inv-inv_mid_2.svg inv.inv_mid_2 Termite Tower Builders
|
||||
[ ] img/inv-inv_mid_3.svg inv.inv_mid_3 Cricket Courier Corp
|
||||
[ ] img/inv-inv_high_1.svg inv.inv_high_1 Locust Swarm Capital
|
||||
[ ] img/inv-inv_high_2.svg inv.inv_high_2 Wasp Venture Fund
|
||||
[ ] img/inv-inv_high_3.svg inv.inv_high_3 Mantis Ambush Futures
|
||||
|
||||
Research (15)
|
||||
[ ] img/research-r_prod_1.svg research.r_prod_1 Optimised Workflows
|
||||
[ ] img/research-r_prod_2.svg research.r_prod_2 Automation Suite
|
||||
[ ] img/research-r_prod_3.svg research.r_prod_3 Total Integration
|
||||
[ ] img/research-r_mkt_1.svg research.r_mkt_1 Market Analysis
|
||||
[ ] img/research-r_mkt_2.svg research.r_mkt_2 Brand Strategy
|
||||
[ ] img/research-r_mkt_3.svg research.r_mkt_3 Premium Positioning
|
||||
[ ] img/research-r_log_1.svg research.r_log_1 Resource Efficiency
|
||||
[ ] img/research-r_log_2.svg research.r_log_2 Bulk Procurement
|
||||
[ ] img/research-r_log_3.svg research.r_log_3 Lean Manufacturing
|
||||
[ ] img/research-r_fin_1.svg research.r_fin_1 Portfolio Management
|
||||
[ ] img/research-r_fin_2.svg research.r_fin_2 Risk Modelling
|
||||
[ ] img/research-r_fin_3.svg research.r_fin_3 Negotiation Training
|
||||
[ ] img/research-r_ins_1.svg research.r_ins_1 Research Methods
|
||||
[ ] img/research-r_ins_2.svg research.r_ins_2 Remote Operations
|
||||
[ ] img/research-r_ins_3.svg research.r_ins_3 Price Oracle
|
||||
|
||||
Legacy perks (6)
|
||||
[ ] img/legacy-lg_prod.svg legacy.lg_prod Industrial Memory
|
||||
[ ] img/legacy-lg_demand.svg legacy.lg_demand Brand Heritage
|
||||
[ ] img/legacy-lg_cost.svg legacy.lg_cost Lean Blueprints
|
||||
[ ] img/legacy-lg_start.svg legacy.lg_start Head Start
|
||||
[ ] img/legacy-lg_offline.svg legacy.lg_offline Night Shift
|
||||
[ ] img/legacy-lg_rp.svg legacy.lg_rp Research Archive
|
||||
|
||||
Trophy (1)
|
||||
[ ] img/trophy.svg trophy A trophy
|
||||
|
||||
|
||||
THEME: computers
|
||||
----------------
|
||||
|
||||
Resources (6)
|
||||
[ ] img/res-units.svg res.units CPU Power
|
||||
[ ] img/res-funds.svg res.funds GalliumCoin
|
||||
[ ] img/res-material.svg res.material Energy (W)
|
||||
[ ] img/res-chips.svg res.chips Qubits
|
||||
[ ] img/res-research.svg res.research Algorithms
|
||||
[ ] img/res-legacy.svg res.legacy Legacy Code
|
||||
|
||||
Automation tiers (6)
|
||||
[ ] img/gen-gen1.svg gen.gen1 Old Laptop
|
||||
[ ] img/gen-gen2.svg gen.gen2 Gaming PC
|
||||
[ ] img/gen-gen3.svg gen.gen3 GPU Mining Rig
|
||||
[ ] img/gen-gen4.svg gen.gen4 Server Rack
|
||||
[ ] img/gen-gen5.svg gen.gen5 Data Center
|
||||
[ ] img/gen-gen6.svg gen.gen6 Quantum Mainframe
|
||||
|
||||
Upgrades (36)
|
||||
[ ] img/upg-click_1.svg upg.click_1 Mechanical Keyboard
|
||||
[ ] img/upg-click_2.svg upg.click_2 Keyboard Macros
|
||||
[ ] img/upg-click_3.svg upg.click_3 Neural Interface
|
||||
[ ] img/upg-g1_a.svg upg.g1_a Thermal Paste
|
||||
[ ] img/upg-g1_b.svg upg.g1_b Fresh OS Install
|
||||
[ ] img/upg-g2_a.svg upg.g2_a Liquid Cooling
|
||||
[ ] img/upg-g2_b.svg upg.g2_b Overclocking
|
||||
[ ] img/upg-g3_a.svg upg.g3_a Custom BIOS
|
||||
[ ] img/upg-g3_b.svg upg.g3_b Memory Timing Tuning
|
||||
[ ] img/upg-g4_a.svg upg.g4_a Hot-Swap Drives
|
||||
[ ] img/upg-g4_b.svg upg.g4_b Load Balancer
|
||||
[ ] img/upg-g5_a.svg upg.g5_a Redundant Power Lines
|
||||
[ ] img/upg-g5_b.svg upg.g5_b Free Cooling (Arctic Site)
|
||||
[ ] img/upg-g6_a.svg upg.g6_a Cryogenic Coolant
|
||||
[ ] img/upg-g6_b.svg upg.g6_b Entangled Qubits
|
||||
[ ] img/upg-global_1.svg upg.global_1 Firmware Update
|
||||
[ ] img/upg-global_2.svg upg.global_2 Parallel Computing
|
||||
[ ] img/upg-global_3.svg upg.global_3 Learning Scheduler
|
||||
[ ] img/upg-global_4.svg upg.global_4 Distributed Grid
|
||||
[ ] img/upg-demand_1.svg upg.demand_1 Forum Hype
|
||||
[ ] img/upg-demand_2.svg upg.demand_2 Tech Influencers
|
||||
[ ] img/upg-demand_3.svg upg.demand_3 Exchange Listing
|
||||
[ ] img/upg-demand_4.svg upg.demand_4 Global Hype Cycle
|
||||
[ ] img/upg-value_1.svg upg.value_1 Shiny Whitepaper
|
||||
[ ] img/upg-value_2.svg upg.value_2 Celebrity Endorsement
|
||||
[ ] img/upg-value_3.svg upg.value_3 To The Moon
|
||||
[ ] img/upg-mat_eff_1.svg upg.mat_eff_1 Efficient PSU
|
||||
[ ] img/upg-mat_eff_2.svg upg.mat_eff_2 Low-Power Chips
|
||||
[ ] img/upg-mat_disc_1.svg upg.mat_disc_1 Off-Peak Contract
|
||||
[ ] img/upg-mat_disc_2.svg upg.mat_disc_2 Own Solar Farm
|
||||
[ ] img/upg-stack_1.svg upg.stack_1 Bigger Batteries
|
||||
[ ] img/upg-stack_2.svg upg.stack_2 Capacitor Banks
|
||||
[ ] img/upg-stack_3.svg upg.stack_3 Grid Substation
|
||||
[ ] img/upg-stack_4.svg upg.stack_4 Dedicated Power Line
|
||||
[ ] img/upg-stack_5.svg upg.stack_5 Private Power Plant
|
||||
[ ] img/upg-auto_buy.svg upg.auto_buy Smart Power Meter
|
||||
|
||||
Endless upgrades (14)
|
||||
[ ] img/endless-e_click.svg endless.e_click Manual mastery
|
||||
[ ] img/endless-e_gen1.svg endless.e_gen1 Old Laptop overhaul
|
||||
[ ] img/endless-e_gen2.svg endless.e_gen2 Gaming PC overhaul
|
||||
[ ] img/endless-e_gen3.svg endless.e_gen3 GPU Mining Rig overhaul
|
||||
[ ] img/endless-e_gen4.svg endless.e_gen4 Server Rack overhaul
|
||||
[ ] img/endless-e_gen5.svg endless.e_gen5 Data Center overhaul
|
||||
[ ] img/endless-e_gen6.svg endless.e_gen6 Quantum Mainframe overhaul
|
||||
[ ] img/endless-e_global.svg endless.e_global Total optimisation
|
||||
[ ] img/endless-e_demand.svg endless.e_demand Mass appeal
|
||||
[ ] img/endless-e_value.svg endless.e_value Premium margin
|
||||
[ ] img/endless-e_efficiency.svg endless.e_efficiency Energy (W) thrift
|
||||
[ ] img/endless-e_discount.svg endless.e_discount Hard bargaining
|
||||
[ ] img/endless-e_stack.svg endless.e_stack Energy (W) stockpiling
|
||||
[ ] img/endless-e_cost.svg endless.e_cost Economies of scale
|
||||
|
||||
Investment societies (9)
|
||||
[ ] img/inv-inv_low_1.svg inv.inv_low_1 Stable Silicon Bank
|
||||
[ ] img/inv-inv_low_2.svg inv.inv_low_2 ByteSafe Trust
|
||||
[ ] img/inv-inv_low_3.svg inv.inv_low_3 Mainframe Heritage Fund
|
||||
[ ] img/inv-inv_mid_1.svg inv.inv_mid_1 NeoChip Holdings
|
||||
[ ] img/inv-inv_mid_2.svg inv.inv_mid_2 CloudNine Hosting
|
||||
[ ] img/inv-inv_mid_3.svg inv.inv_mid_3 Packet Dynamics
|
||||
[ ] img/inv-inv_high_1.svg inv.inv_high_1 MoonShot Mining DAO
|
||||
[ ] img/inv-inv_high_2.svg inv.inv_high_2 HypeCoin Labs
|
||||
[ ] img/inv-inv_high_3.svg inv.inv_high_3 Quantum Leap Capital
|
||||
|
||||
Research (15)
|
||||
[ ] img/research-r_prod_1.svg research.r_prod_1 Optimised Workflows
|
||||
[ ] img/research-r_prod_2.svg research.r_prod_2 Automation Suite
|
||||
[ ] img/research-r_prod_3.svg research.r_prod_3 Total Integration
|
||||
[ ] img/research-r_mkt_1.svg research.r_mkt_1 Market Analysis
|
||||
[ ] img/research-r_mkt_2.svg research.r_mkt_2 Brand Strategy
|
||||
[ ] img/research-r_mkt_3.svg research.r_mkt_3 Premium Positioning
|
||||
[ ] img/research-r_log_1.svg research.r_log_1 Resource Efficiency
|
||||
[ ] img/research-r_log_2.svg research.r_log_2 Bulk Procurement
|
||||
[ ] img/research-r_log_3.svg research.r_log_3 Lean Manufacturing
|
||||
[ ] img/research-r_fin_1.svg research.r_fin_1 Portfolio Management
|
||||
[ ] img/research-r_fin_2.svg research.r_fin_2 Risk Modelling
|
||||
[ ] img/research-r_fin_3.svg research.r_fin_3 Negotiation Training
|
||||
[ ] img/research-r_ins_1.svg research.r_ins_1 Research Methods
|
||||
[ ] img/research-r_ins_2.svg research.r_ins_2 Remote Operations
|
||||
[ ] img/research-r_ins_3.svg research.r_ins_3 Price Oracle
|
||||
|
||||
Legacy perks (6)
|
||||
[ ] img/legacy-lg_prod.svg legacy.lg_prod Industrial Memory
|
||||
[ ] img/legacy-lg_demand.svg legacy.lg_demand Brand Heritage
|
||||
[ ] img/legacy-lg_cost.svg legacy.lg_cost Lean Blueprints
|
||||
[ ] img/legacy-lg_start.svg legacy.lg_start Head Start
|
||||
[ ] img/legacy-lg_offline.svg legacy.lg_offline Night Shift
|
||||
[ ] img/legacy-lg_rp.svg legacy.lg_rp Research Archive
|
||||
|
||||
Trophy (1)
|
||||
[ ] img/trophy.svg trophy A trophy
|
||||
@@ -32,7 +32,8 @@ public/
|
||||
engine/ game rules, pure JS, no DOM (also used by the tests): engine (production, market,
|
||||
time) + one module per system (investments, gamble, research, legacy, contracts, achievements)
|
||||
services/ cookies · storage · save import/export · i18n · formatting · data loading
|
||||
ui/ DOM helpers · main view · one view per section · tabs/menu · settings · toasts
|
||||
ui/ DOM helpers · main view · one view per section · tabs/menu · settings · toasts ·
|
||||
fx (the quick purchase animations)
|
||||
main.js wires everything together
|
||||
tests/ node:test suites (engine, systems, save import/export, server, content completeness)
|
||||
```
|
||||
@@ -92,189 +93,13 @@ Everything numeric is in `public/data/`: `rules.json` (market, raw material, mar
|
||||
|
||||
## Icons to produce
|
||||
|
||||
Every icon the game shows is a grey square until a theme provides an image. Each theme needs the **same set of icons, drawn in its own style**: the list below is generated from the game data, so it is always complete.
|
||||
Every icon the game shows is a grey square until a theme provides an image. The exhaustive work list is in **[ICONS.txt](ICONS.txt)**: a checklist per theme, with the file name, the key and what to draw for each icon. It is generated from the game data, so it is always complete. All 23 achievements share a single `trophy` icon.
|
||||
|
||||
**Format.** One square image per icon: SVG preferred (or PNG at 128×128 px or more), transparent background, readable at 28 px (top bar) and 48 px (lists). Put the files in `public/themes/<theme>/img/`.
|
||||
|
||||
**Plugging them in.** List each file in the theme's `theme.json`; anything not listed keeps the grey square, so images can be added a few at a time:
|
||||
- **Format:** one square image per icon, SVG preferred (or PNG at 128×128 px or more), transparent background, readable at 28 px (top bar) and 48 px (lists). Files go in `public/themes/<theme>/img/`.
|
||||
- **Plugging them in:** list each file in the theme's `theme.json`; anything not listed keeps the grey square, so images can be added a few at a time:
|
||||
|
||||
```json
|
||||
{ "order": 1, "assets": { "res.units": "img/res-units.svg", "gen.gen1": "img/gen-gen1.svg" } }
|
||||
```
|
||||
|
||||
`npm run icons -- --manifest` prints a complete `assets` skeleton (every key mapped to `img/<key>.svg`) to start from. `npm run icons` prints the list below, and `npm run icons -- --write` refreshes it in this file; `npm test` fails if it is out of date or if the interface uses an icon key that is not listed.
|
||||
|
||||
The tables give what each icon represents in each theme (the name used in the game, in English).
|
||||
|
||||
<!-- icons:start (generated by `npm run icons -- --write`, do not edit by hand) -->
|
||||
|
||||
**115 icons per theme, 575 in total** (5 themes: `paperclip`, `witch`, `alien`, `insects`, `computers`).
|
||||
|
||||
| Group | Icons per theme | Shown in |
|
||||
|---|---:|---|
|
||||
| Resources | 6 | top bar (units, funds, material), Gamble, Research and Legacy screens |
|
||||
| Automation tiers | 6 | Production screen, Automation list |
|
||||
| Upgrades | 36 | Production screen, Upgrades list |
|
||||
| Investment societies | 9 | Investments screen |
|
||||
| Endless upgrades | 14 | Production screen, Endless upgrades panel |
|
||||
| Research | 15 | Research screen |
|
||||
| Legacy perks | 6 | Legacy screen |
|
||||
| Achievements | 23 | Achievements screen |
|
||||
| **Total** | **115** | |
|
||||
|
||||
#### Resources (6)
|
||||
|
||||
| Key | `paperclip` | `witch` | `alien` | `insects` | `computers` |
|
||||
|---|---|---|---|---|---|
|
||||
| `res.units` | Paperclips | Potions | Xenocrystals | Leaves | CPU Power |
|
||||
| `res.funds` | Funds | Gold | Credits | Honeydew | GalliumCoin |
|
||||
| `res.material` | Wire | Herbs | Raw ore | Silk | Energy (W) |
|
||||
| `res.chips` | Quantum Chips | Fate Tokens | Chance Crystals | Pollen Grains | Qubits |
|
||||
| `res.research` | R&D Points | Arcane Insight | Xeno-Data | Instinct | Algorithms |
|
||||
| `res.legacy` | Patents | Grimoire Pages | Blueprints | Colony Memories | Legacy Code |
|
||||
|
||||
#### Automation tiers (6)
|
||||
|
||||
| Key | `paperclip` | `witch` | `alien` | `insects` | `computers` |
|
||||
|---|---|---|---|---|---|
|
||||
| `gen.gen1` | Autoclipper | Apprentice | Mining Drone | Worker Ant | Old Laptop |
|
||||
| `gen.gen2` | MegaClipper | Bubbling Cauldron | Plasma Drill | Leafcutter Squad | Gaming PC |
|
||||
| `gen.gen3` | Clip Factory | Alchemy Lab | Orbital Extractor | Beetle Hauler | GPU Mining Rig |
|
||||
| `gen.gen4` | Hypno Drones | Broomstick Fleet | Asteroid Harvester | Termite Mound | Server Rack |
|
||||
| `gen.gen5` | Probe Swarm | Moonlit Coven | Hive Swarm | Locust Swarm | Data Center |
|
||||
| `gen.gen6` | Matter Converter | Philosopher's Engine | Planet Cracker | Hive Queen | Quantum Mainframe |
|
||||
|
||||
#### Upgrades (36)
|
||||
|
||||
| Key | `paperclip` | `witch` | `alien` | `insects` | `computers` |
|
||||
|---|---|---|---|---|---|
|
||||
| `upg.click_1` | Ergonomic Bender | Steady Stirring | Gravity Gloves | Sharper Mandibles | Mechanical Keyboard |
|
||||
| `upg.click_2` | Pneumatic Grip | Enchanted Ladle | Exo-Suit | Pheromone Trail | Keyboard Macros |
|
||||
| `upg.click_3` | Robotic Arm | Familiar's Help | Neural Link | Double-Grip Legs | Neural Interface |
|
||||
| `upg.g1_a` | Sharper Autoclippers | Apprentice Lessons | Drone Firmware | Ant Drill Sergeant | Thermal Paste |
|
||||
| `upg.g1_b` | Overclocked Autoclippers | Apprentice Mastery | Drone Swarm Logic | Marching Songs | Fresh OS Install |
|
||||
| `upg.g2_a` | MegaClipper Servos | Copper Cauldrons | Diamond Bits | Wider Leaf Slices | Liquid Cooling |
|
||||
| `upg.g2_b` | MegaClipper Turbines | Eternal Flame | Plasma Focusing | Leafcutter Guild | Overclocking |
|
||||
| `upg.g3_a` | Assembly Line | Crystal Alembics | Tether Cables | Reinforced Shells | Custom BIOS |
|
||||
| `upg.g3_b` | Just-in-Time Logistics | Rune-Etched Tables | Zero-G Smelting | Dung Beetle Logistics | Memory Timing Tuning |
|
||||
| `upg.g4_a` | Hypno Harmonics | Waxed Brooms | Mass Drivers | Air-Conditioned Chambers | Hot-Swap Drives |
|
||||
| `upg.g4_b` | Persuasion Algorithms | Hex-Powered Flight | Autonomous Prospecting | Deep Tunnel Network | Load Balancer |
|
||||
| `upg.g5_a` | Probe Self-Replication | Full Moon Rites | Hive Mind Sync | Riding the Wind | Redundant Power Lines |
|
||||
| `upg.g5_b` | Von Neumann Protocols | Blood Moon Pact | Queen Directive | Migration Instinct | Free Cooling (Arctic Site) |
|
||||
| `upg.g6_a` | Atomic Reassembly | Transmutation Circles | Tectonic Resonance | Royal Jelly | Cryogenic Coolant |
|
||||
| `upg.g6_b` | Universal Reformatting | The Fifth Element | Core Shattering | Queen's Decree | Entangled Qubits |
|
||||
| `upg.global_1` | Quality Control | Spellbook Index | Quantum Diagnostics | Dewdrop Hydration | Firmware Update |
|
||||
| `upg.global_2` | Lean Manufacturing | Grandmother's Grimoire | Alien Alloys | Seasonal Rhythm | Parallel Computing |
|
||||
| `upg.global_3` | Industrial AI | Ley Line Tap | Dyson Tap | Hive Mind | Learning Scheduler |
|
||||
| `upg.global_4` | Quantum Scheduling | Time Dilation Charm | Spacetime Folding | Perfect Spring | Distributed Grid |
|
||||
| `upg.demand_1` | Newspaper Ad | Village Notice Board | Trade Beacon | Scent Markers | Forum Hype |
|
||||
| `upg.demand_2` | Radio Campaign | Market Stall | Interstellar Broker | Cricket Courier | Tech Influencers |
|
||||
| `upg.demand_3` | Viral Video | Royal Commission | Galactic Exchange | Forest Market | Exchange Listing |
|
||||
| `upg.demand_4` | Global Brand | Legend of the Witch | Cosmic Monopoly | Famous Pile | Global Hype Cycle |
|
||||
| `upg.value_1` | Gold-Plated Paperclips | Glittering Labels | Purity Certificate | Dewy Gloss | Shiny Whitepaper |
|
||||
| `upg.value_2` | Premium Packaging | Apothecary Seal | Rare Spectrum Cut | Autumn Colours | Celebrity Endorsement |
|
||||
| `upg.value_3` | Luxury Branding | Royal Warrant | Imperial Grade | Golden Leaf Grade | To The Moon |
|
||||
| `upg.mat_eff_1` | Thinner Wire | Careful Pruning | Ore Analyzer | Tighter Weaving | Efficient PSU |
|
||||
| `upg.mat_eff_2` | Wire Recycling | Distilled Essence | Molecular Sieve | Silk Recycling | Low-Power Chips |
|
||||
| `upg.mat_disc_1` | Bulk Wire Contract | Forager's Pact | Asteroid Contract | Spider Contract | Off-Peak Contract |
|
||||
| `upg.mat_disc_2` | Direct From the Mill | Enchanted Garden | Own the Mine | Silkworm Farm | Own Solar Farm |
|
||||
| `upg.stack_1` | Bigger Spools | Larger Baskets | Bigger Cargo Pods | Bigger Silk Bundles | Bigger Batteries |
|
||||
| `upg.stack_2` | Pallet Orders | Herb Sacks | Container Drones | Cocoon Sacks | Capacitor Banks |
|
||||
| `upg.stack_3` | Container Loads | Cartloads | Freighter Holds | Web Warehouses | Grid Substation |
|
||||
| `upg.stack_4` | Freight Trains | Greenhouse Harvest | Convoy Fleets | Spider Caravans | Dedicated Power Line |
|
||||
| `upg.stack_5` | Wire Mills | Whole-Forest Rights | Orbital Stockpile | Great Silk Hoard | Private Power Plant |
|
||||
| `upg.auto_buy` | WireBuyer | Self-Gathering Basket | Autonomous Freighter | Spider Delivery Service | Smart Power Meter |
|
||||
|
||||
#### Investment societies (9)
|
||||
|
||||
| Key | `paperclip` | `witch` | `alien` | `insects` | `computers` |
|
||||
|---|---|---|---|---|---|
|
||||
| `inv.inv_low_1` | Steady Staples Inc. | Old Hollow Apothecary | Zeta Mining Cooperative | Anthill Savings Bank | Stable Silicon Bank |
|
||||
| `inv.inv_low_2` | Office Supply Trust | Greywood Herb Co-op | Orion Ore Trust | Honeycomb Trust | ByteSafe Trust |
|
||||
| `inv.inv_low_3` | Copper Wire Bond | Village Cauldron Guild | Lunar Refinery Bond | Leafy Greens Co-op | Mainframe Heritage Fund |
|
||||
| `inv.inv_mid_1` | Binder Dynamics | Moonpetal Brewers | XenoCorp Industries | Dung Beetle Logistics Ltd | NeoChip Holdings |
|
||||
| `inv.inv_mid_2` | Clip Logistics | Nightshade & Sons | Nebula Extraction Ltd | Termite Tower Builders | CloudNine Hosting |
|
||||
| `inv.inv_mid_3` | Stationery Futures | Crooked Broom Trading | Void Cargo Syndicate | Cricket Courier Corp | Packet Dynamics |
|
||||
| `inv.inv_high_1` | NanoWire Labs | Dragonscale Elixirs | Hyperion Rift Labs | Locust Swarm Capital | MoonShot Mining DAO |
|
||||
| `inv.inv_high_2` | Paperless Dreams Ltd | Wyrmwood Potions Ltd | Singularity Salvage | Wasp Venture Fund | HypeCoin Labs |
|
||||
| `inv.inv_high_3` | Hyper-Clip Ventures | Forbidden Brew Cartel | Black Hole Ventures | Mantis Ambush Futures | Quantum Leap Capital |
|
||||
|
||||
#### Endless upgrades (14)
|
||||
|
||||
| Key | `paperclip` | `witch` | `alien` | `insects` | `computers` |
|
||||
|---|---|---|---|---|---|
|
||||
| `endless.e_click` | Manual mastery | Manual mastery | Manual mastery | Manual mastery | Manual mastery |
|
||||
| `endless.e_gen1` | Autoclipper overhaul | Apprentice overhaul | Mining Drone overhaul | Worker Ant overhaul | Old Laptop overhaul |
|
||||
| `endless.e_gen2` | MegaClipper overhaul | Bubbling Cauldron overhaul | Plasma Drill overhaul | Leafcutter Squad overhaul | Gaming PC overhaul |
|
||||
| `endless.e_gen3` | Clip Factory overhaul | Alchemy Lab overhaul | Orbital Extractor overhaul | Beetle Hauler overhaul | GPU Mining Rig overhaul |
|
||||
| `endless.e_gen4` | Hypno Drones overhaul | Broomstick Fleet overhaul | Asteroid Harvester overhaul | Termite Mound overhaul | Server Rack overhaul |
|
||||
| `endless.e_gen5` | Probe Swarm overhaul | Moonlit Coven overhaul | Hive Swarm overhaul | Locust Swarm overhaul | Data Center overhaul |
|
||||
| `endless.e_gen6` | Matter Converter overhaul | Philosopher's Engine overhaul | Planet Cracker overhaul | Hive Queen overhaul | Quantum Mainframe overhaul |
|
||||
| `endless.e_global` | Total optimisation | Total optimisation | Total optimisation | Total optimisation | Total optimisation |
|
||||
| `endless.e_demand` | Mass appeal | Mass appeal | Mass appeal | Mass appeal | Mass appeal |
|
||||
| `endless.e_value` | Premium margin | Premium margin | Premium margin | Premium margin | Premium margin |
|
||||
| `endless.e_efficiency` | Wire thrift | Herbs thrift | Raw ore thrift | Silk thrift | Energy (W) thrift |
|
||||
| `endless.e_discount` | Hard bargaining | Hard bargaining | Hard bargaining | Hard bargaining | Hard bargaining |
|
||||
| `endless.e_stack` | Wire stockpiling | Herbs stockpiling | Raw ore stockpiling | Silk stockpiling | Energy (W) stockpiling |
|
||||
| `endless.e_cost` | Economies of scale | Economies of scale | Economies of scale | Economies of scale | Economies of scale |
|
||||
|
||||
#### Research (15)
|
||||
|
||||
| Key | Subject (the same in every theme, drawn in each theme's style) |
|
||||
|---|---|
|
||||
| `research.r_prod_1` | Optimised Workflows |
|
||||
| `research.r_prod_2` | Automation Suite |
|
||||
| `research.r_prod_3` | Total Integration |
|
||||
| `research.r_mkt_1` | Market Analysis |
|
||||
| `research.r_mkt_2` | Brand Strategy |
|
||||
| `research.r_mkt_3` | Premium Positioning |
|
||||
| `research.r_log_1` | Resource Efficiency |
|
||||
| `research.r_log_2` | Bulk Procurement |
|
||||
| `research.r_log_3` | Lean Manufacturing |
|
||||
| `research.r_fin_1` | Portfolio Management |
|
||||
| `research.r_fin_2` | Risk Modelling |
|
||||
| `research.r_fin_3` | Negotiation Training |
|
||||
| `research.r_ins_1` | Research Methods |
|
||||
| `research.r_ins_2` | Remote Operations |
|
||||
| `research.r_ins_3` | Price Oracle |
|
||||
|
||||
#### Legacy perks (6)
|
||||
|
||||
| Key | Subject (the same in every theme, drawn in each theme's style) |
|
||||
|---|---|
|
||||
| `legacy.lg_prod` | Industrial Memory |
|
||||
| `legacy.lg_demand` | Brand Heritage |
|
||||
| `legacy.lg_cost` | Lean Blueprints |
|
||||
| `legacy.lg_start` | Head Start |
|
||||
| `legacy.lg_offline` | Night Shift |
|
||||
| `legacy.lg_rp` | Research Archive |
|
||||
|
||||
#### Achievements (23)
|
||||
|
||||
| Key | Subject (the same in every theme, drawn in each theme's style) |
|
||||
|---|---|
|
||||
| `ach.a_prod_1` | First Steps |
|
||||
| `ach.a_prod_2` | Getting Serious |
|
||||
| `ach.a_prod_3` | Industrial Scale |
|
||||
| `ach.a_prod_4` | Beyond Imagination |
|
||||
| `ach.a_prod_5` | Cosmic Output |
|
||||
| `ach.a_gen_1` | Small Team |
|
||||
| `ach.a_gen_2` | Big Operation |
|
||||
| `ach.a_gen_3` | Endless Workforce |
|
||||
| `ach.a_upg_1` | Tinkerer |
|
||||
| `ach.a_upg_2` | Perfectionist |
|
||||
| `ach.a_inv_1` | Pocket Money |
|
||||
| `ach.a_inv_2` | Smart Money |
|
||||
| `ach.a_inv_3` | Tycoon |
|
||||
| `ach.a_gam_1` | Beginner's Luck |
|
||||
| `ach.a_gam_2` | High Roller |
|
||||
| `ach.a_gam_3` | Chip Collector |
|
||||
| `ach.a_con_1` | Deal Maker |
|
||||
| `ach.a_con_2` | Reliable Partner |
|
||||
| `ach.a_res_1` | Curious Mind |
|
||||
| `ach.a_res_2` | Master Scientist |
|
||||
| `ach.a_leg_1` | Fresh Start |
|
||||
| `ach.a_leg_2` | Eternal Return |
|
||||
| `ach.a_evt_1` | Quick Hands |
|
||||
|
||||
<!-- icons:end -->
|
||||
- `npm run icons -- --write` regenerates `ICONS.txt`, `npm run icons -- --manifest` prints a complete `assets` skeleton to start from. `npm test` fails if `ICONS.txt` is out of date or if the interface uses an icon key that is not listed.
|
||||
|
||||
+154
-12
@@ -128,16 +128,6 @@
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.btn--buy {
|
||||
flex-direction: column;
|
||||
min-width: 7.5rem;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.btn__label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- form fields */
|
||||
|
||||
.field {
|
||||
@@ -312,7 +302,7 @@ select,
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Narrow phones: the buy button drops under the text. */
|
||||
/* Narrow phones: the price drops under the text. */
|
||||
@media (max-width: 479px) {
|
||||
.card {
|
||||
flex-wrap: wrap;
|
||||
@@ -322,10 +312,14 @@ select,
|
||||
flex-basis: calc(100% - var(--icon-size) - var(--gap));
|
||||
}
|
||||
|
||||
.btn--buy {
|
||||
.card__price {
|
||||
flex: 1 0 100%;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
padding-top: 0.35rem;
|
||||
border-top: 1px solid var(--border);
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,3 +636,151 @@ select,
|
||||
.tile .hint {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------ blocks you click to buy */
|
||||
|
||||
.card--buy {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition:
|
||||
box-shadow 0.12s ease,
|
||||
transform 0.08s ease,
|
||||
opacity 0.12s ease;
|
||||
}
|
||||
|
||||
/* A soft outline on hover (only where there is a real hover: not on touch screens). */
|
||||
@media (hover: hover) {
|
||||
.card--buy:hover:not(.is-disabled) {
|
||||
box-shadow: 0 0 0 2px var(--accent);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 55%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.card--buy:active:not(.is-disabled) {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.card--buy.is-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* What it costs, on the right of the block; a fixed width keeps the figures aligned. */
|
||||
.card__price {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
align-self: center;
|
||||
gap: 0.1rem;
|
||||
min-width: 6.5rem;
|
||||
margin-left: auto;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card__price strong {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.card__price small {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.card__hint {
|
||||
align-self: center;
|
||||
margin-right: auto;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- purchase animations */
|
||||
/* Quick on purpose (0.15 to 0.25 s): feedback, not a show. */
|
||||
|
||||
@keyframes fx-pop {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 var(--accent);
|
||||
}
|
||||
40% {
|
||||
transform: scale(1.04);
|
||||
box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 45%, transparent);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fx-bump {
|
||||
50% {
|
||||
transform: scale(1.35);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fx-spend {
|
||||
from {
|
||||
color: var(--bad);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes floater {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, calc(-100% - 26px));
|
||||
}
|
||||
}
|
||||
|
||||
.fx-pop {
|
||||
animation: fx-pop 0.2s ease-out;
|
||||
}
|
||||
|
||||
.fx-bump {
|
||||
animation: fx-bump 0.15s ease-out;
|
||||
}
|
||||
|
||||
.fx-spend {
|
||||
animation: fx-spend 0.2s ease-out;
|
||||
}
|
||||
|
||||
.floater {
|
||||
position: fixed;
|
||||
z-index: 20;
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
pointer-events: none;
|
||||
text-shadow: 0 1px 2px rgb(0 0 0 / 0.3);
|
||||
animation: floater 0.25s ease-out forwards;
|
||||
}
|
||||
|
||||
.floater--good {
|
||||
color: var(--good);
|
||||
}
|
||||
|
||||
.floater--bad {
|
||||
color: var(--bad);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.fx-pop,
|
||||
.fx-bump,
|
||||
.fx-spend {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.floater {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card--buy {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
<label><input type="radio" name="buy-amount" value="max"><span data-i18n="ui.max"></span></label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<p class="hint" data-i18n="ui.clickToBuy"></p>
|
||||
<ul class="cards" id="generators"></ul>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -124,7 +124,6 @@ export function stepEvents(state, ctx, dt, d, rng, offline, totals) {
|
||||
totals.notices.push({ kind: 'event_incident', type: type.id });
|
||||
} else {
|
||||
ev.current = { type: type.id, remaining: cfg.claimSeconds };
|
||||
totals.notices.push({ kind: 'event_opportunity', type: type.id });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -166,9 +166,6 @@ function announce(notice) {
|
||||
case 'contract_failed':
|
||||
showToast(t('toast.contractFailed'), { error: true });
|
||||
break;
|
||||
case 'event_opportunity':
|
||||
showToast(t('toast.eventOpportunity', { title: t(`event.${notice.type}.title`) }));
|
||||
break;
|
||||
case 'event_incident':
|
||||
showToast(t('toast.eventIncident', { title: t(`event.${notice.type}.title`) }), { error: true });
|
||||
break;
|
||||
@@ -221,9 +218,11 @@ function render() {
|
||||
|
||||
// ------------------------------------------------------------------- actions
|
||||
|
||||
/** Wrap an engine action: run it on the current state, refresh the screen, report if it happened. */
|
||||
const act = (fn) => (...args) => {
|
||||
fn(state, ctx, ...args);
|
||||
const done = fn(state, ctx, ...args);
|
||||
render();
|
||||
return done;
|
||||
};
|
||||
|
||||
const view = createView({
|
||||
|
||||
@@ -23,7 +23,7 @@ export function createAchievementsView(root) {
|
||||
const li = h(
|
||||
'li',
|
||||
{ class: 'card' },
|
||||
icon(`ach.${a.id}`),
|
||||
icon('trophy'),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'card__body' },
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// and the board of offers.
|
||||
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h } from './dom.js';
|
||||
import { h, makeBuyable, setBuyable } from './dom.js';
|
||||
import { purchased } from './fx.js';
|
||||
import { formatDuration, moneyFormatter, rewardText, setText } from './text.js';
|
||||
|
||||
const BUFF_LABEL = {
|
||||
@@ -133,24 +134,31 @@ export function createContractsView(root, actions) {
|
||||
if (signature !== offersSignature) {
|
||||
offersSignature = signature;
|
||||
els.offers.replaceChildren(
|
||||
...offers.map((offer, index) =>
|
||||
h(
|
||||
...offers.map((offer, index) => {
|
||||
const li = h(
|
||||
'li',
|
||||
{ class: 'card' },
|
||||
h(
|
||||
'div',
|
||||
{ class: 'card__body' },
|
||||
h('h3', { class: 'card__title', text: t(`contracts.template.${offer.template}`) }),
|
||||
h('p', { class: 'card__desc', text: t('contracts.goal', { target: fmt.number(offer.target), duration: formatDuration(offer.seconds) }) }),
|
||||
h('p', {
|
||||
class: 'card__desc',
|
||||
text: t('contracts.goal', { target: fmt.number(offer.target), duration: formatDuration(offer.seconds) }),
|
||||
}),
|
||||
h('p', { class: 'card__meta', text: rewardText(offer.reward, fmt, offer.seconds) }),
|
||||
),
|
||||
h('button', { class: 'btn btn--buy', type: 'button', text: t('contracts.accept'), 'data-index': index, onclick: () => actions.accept(index) }),
|
||||
),
|
||||
),
|
||||
h('div', { class: 'card__price' }, h('strong', { text: t('contracts.accept') })),
|
||||
);
|
||||
makeBuyable(li, () => {
|
||||
if (actions.accept(index)) purchased(li, '✓', null);
|
||||
});
|
||||
return li;
|
||||
}),
|
||||
);
|
||||
}
|
||||
els.offersNone.hidden = offers.length > 0;
|
||||
for (const button of els.offers.querySelectorAll('button')) button.disabled = Boolean(state.contracts.active);
|
||||
for (const li of els.offers.children) setBuyable(li, !state.contracts.active);
|
||||
}
|
||||
|
||||
function update(state) {
|
||||
|
||||
@@ -39,3 +39,31 @@ export function icon(key) {
|
||||
export function refreshStaticIcons(root = document) {
|
||||
for (const el of root.querySelectorAll('.resourcebar [data-icon]')) el.replaceWith(icon(el.dataset.icon));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ buyable blocks
|
||||
// A block you click to buy (instead of a button inside it): it is announced as
|
||||
// a button, reachable with the keyboard (Enter / Space), outlined when hovered
|
||||
// and greyed out when it cannot be bought.
|
||||
|
||||
/** Make `el` act as one big button. `activate(event)` runs on click, Enter or Space. */
|
||||
export function makeBuyable(el, activate) {
|
||||
el.classList.add('card--buy');
|
||||
el.setAttribute('role', 'button');
|
||||
el.tabIndex = 0;
|
||||
const allowed = () => el.getAttribute('aria-disabled') !== 'true';
|
||||
el.addEventListener('click', (event) => {
|
||||
if (allowed()) activate(event);
|
||||
});
|
||||
el.addEventListener('keydown', (event) => {
|
||||
if ((event.key === 'Enter' || event.key === ' ') && event.target === el) {
|
||||
event.preventDefault();
|
||||
if (allowed()) activate(event);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Show whether a buyable block can currently be bought. */
|
||||
export function setBuyable(el, canBuy) {
|
||||
el.classList.toggle('is-disabled', !canBuy);
|
||||
el.setAttribute('aria-disabled', String(!canBuy));
|
||||
}
|
||||
|
||||
+17
-14
@@ -2,7 +2,8 @@
|
||||
|
||||
import { endlessQuote, lineAvailable, lineLevel } from '../engine/endless.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h, icon } from './dom.js';
|
||||
import { h, icon, makeBuyable, setBuyable } from './dom.js';
|
||||
import { bump, purchased } from './fx.js';
|
||||
import { effectText, moneyFormatter, setText } from './text.js';
|
||||
|
||||
const AMOUNTS = [1, 10, 100, 'max'];
|
||||
@@ -22,14 +23,8 @@ export function createEndlessView(root, actions) {
|
||||
function rowFor(line) {
|
||||
const level = h('span', { class: 'badge' });
|
||||
const total = h('p', { class: 'card__meta' });
|
||||
const label = h('span', { class: 'btn__label' });
|
||||
const label = h('strong');
|
||||
const cost = h('small');
|
||||
const buy = h(
|
||||
'button',
|
||||
{ class: 'btn btn--buy', type: 'button', onclick: () => actions.buy(line.id, amount) },
|
||||
label,
|
||||
cost,
|
||||
);
|
||||
const li = h(
|
||||
'li',
|
||||
{ class: 'card', hidden: true },
|
||||
@@ -41,9 +36,15 @@ export function createEndlessView(root, actions) {
|
||||
h('p', { class: 'card__desc', text: t('endless.perLevel', { effect: effectText(line.effect, fmt) }) }),
|
||||
total,
|
||||
),
|
||||
buy,
|
||||
h('div', { class: 'card__price' }, label, cost),
|
||||
);
|
||||
return { li, level, total, label, cost, buy, line };
|
||||
const row = { li, level, total, label, cost, line, shown: 1 };
|
||||
makeBuyable(li, () => {
|
||||
if (!actions.buy(line.id, amount)) return;
|
||||
purchased(li, `+${fmt.number(row.shown)}`);
|
||||
bump(level);
|
||||
});
|
||||
return row;
|
||||
}
|
||||
|
||||
function rebuild(newCtx, formatter) {
|
||||
@@ -87,14 +88,15 @@ export function createEndlessView(root, actions) {
|
||||
h('h2', { class: 'panel__title', text: t('endless.title') }),
|
||||
amountGroup,
|
||||
),
|
||||
h('p', { class: 'hint', text: t('endless.intro') }),
|
||||
h('p', { class: 'hint', text: `${t('endless.intro')} ${t('ui.clickToBuy')}` }),
|
||||
list,
|
||||
);
|
||||
}
|
||||
|
||||
function update(state) {
|
||||
let any = false;
|
||||
for (const { li, level, total, label, cost, buy, line } of rows.values()) {
|
||||
for (const row of rows.values()) {
|
||||
const { li, level, total, label, cost, line } = row;
|
||||
const open = lineAvailable(state, line);
|
||||
li.hidden = !open;
|
||||
if (!open) continue;
|
||||
@@ -112,9 +114,10 @@ export function createEndlessView(root, actions) {
|
||||
? t('endless.total', { effect: effectText({ ...line.effect, value: line.effect.value ** lv }, fmt) })
|
||||
: t('endless.none'),
|
||||
);
|
||||
setText(label, maxed ? t('endless.maxed') : t('endless.buy', { count: fmt.number(quote.count) }));
|
||||
row.shown = quote.count;
|
||||
setText(label, maxed ? t('endless.maxed') : `×${fmt.number(quote.count)}`);
|
||||
setText(cost, maxed ? '' : money(quote.cost));
|
||||
buy.disabled = maxed || quote.count <= 0 || !(state.funds >= quote.cost);
|
||||
setBuyable(li, !maxed && quote.count > 0 && state.funds >= quote.cost);
|
||||
}
|
||||
root.hidden = !any;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Quick feedback animations (0.15 to 0.25 s), so buying feels immediate.
|
||||
// All of them are plain CSS animations restarted from here; users who prefer
|
||||
// reduced motion get none (see components.css).
|
||||
|
||||
const FALLBACK_MS = 400; // clean-up if the browser never fires animationend
|
||||
|
||||
/** Restart a one-shot CSS animation class on an element. */
|
||||
function play(el, className) {
|
||||
if (!el) return;
|
||||
el.classList.remove(className);
|
||||
void el.offsetWidth; // let the browser notice the class was removed
|
||||
el.classList.add(className);
|
||||
const done = () => el.classList.remove(className);
|
||||
el.addEventListener('animationend', done, { once: true });
|
||||
setTimeout(done, FALLBACK_MS);
|
||||
}
|
||||
|
||||
/** A block that was just bought: a quick pop and an accent glow. */
|
||||
export const pop = (el) => play(el, 'fx-pop');
|
||||
|
||||
/** A number that just changed: a quick bump. */
|
||||
export const bump = (el) => play(el, 'fx-bump');
|
||||
|
||||
/** A resource that was just spent: a quick flash. */
|
||||
export const spend = (el) => play(el, 'fx-spend');
|
||||
|
||||
/**
|
||||
* A short "+3" that rises from the middle of `anchor` (or from a point) and fades.
|
||||
* `at` = { x, y } in viewport coordinates, used when given instead of an element.
|
||||
* `tone` ('good' | 'bad') colours it green or red; by default it takes the accent colour.
|
||||
*/
|
||||
export function floater(anchor, text, at, tone) {
|
||||
const rect = at ? { left: at.x, top: at.y, width: 0 } : anchor?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
const el = document.createElement('span');
|
||||
el.className = tone ? `floater floater--${tone}` : 'floater';
|
||||
el.textContent = text;
|
||||
el.style.left = `${rect.left + rect.width / 2}px`;
|
||||
el.style.top = `${rect.top}px`;
|
||||
document.body.append(el);
|
||||
el.addEventListener('animationend', () => el.remove(), { once: true });
|
||||
setTimeout(() => el.remove(), FALLBACK_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop a bought block, float a label above it and flash what was spent (the funds
|
||||
* in the top bar unless `spentId` says otherwise, or is null for no flash).
|
||||
*/
|
||||
export function purchased(el, label, spentId = 'v-funds') {
|
||||
pop(el);
|
||||
floater(el, label);
|
||||
if (spentId) spend(document.getElementById(spentId));
|
||||
return true;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { chipCost, odds, reliabilityCost } from '../engine/gamble.js';
|
||||
import { chipDemandBonus } from '../engine/formulas.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h, icon } from './dom.js';
|
||||
import { bump, floater, pop, purchased } from './fx.js';
|
||||
import { moneyFormatter, setText } from './text.js';
|
||||
|
||||
const STAKES = [
|
||||
@@ -43,6 +44,10 @@ export function createGambleView(root, actions) {
|
||||
const result = actions.run(stakeCount(state.chips));
|
||||
if (!result) return;
|
||||
log.unshift(result);
|
||||
pop(els.run);
|
||||
const sign = result.delta > 0 ? '+' : '−';
|
||||
floater(els.run, `${sign}${fmt.number(Math.abs(result.delta))}`, undefined, result.delta > 0 ? 'good' : 'bad');
|
||||
bump(els.chips);
|
||||
log.length = Math.min(log.length, LOG_SIZE);
|
||||
renderLog();
|
||||
}
|
||||
@@ -57,8 +62,23 @@ export function createGambleView(root, actions) {
|
||||
els = {
|
||||
chips: h('strong'),
|
||||
bonus: h('span', { class: 'hint' }),
|
||||
buyChip: h('button', { class: 'btn btn--wide', type: 'button', onclick: () => actions.buyChip() }),
|
||||
reliability: h('button', { class: 'btn btn--wide', type: 'button', onclick: () => actions.buyReliability() }),
|
||||
buyChip: h('button', {
|
||||
class: 'btn btn--wide',
|
||||
type: 'button',
|
||||
onclick: () => {
|
||||
if (actions.buyChip()) {
|
||||
purchased(els.buyChip, '+1');
|
||||
bump(els.chips);
|
||||
}
|
||||
},
|
||||
}),
|
||||
reliability: h('button', {
|
||||
class: 'btn btn--wide',
|
||||
type: 'button',
|
||||
onclick: () => {
|
||||
if (actions.buyReliability()) purchased(els.reliability, '+1');
|
||||
},
|
||||
}),
|
||||
odds: h('p', { class: 'hint' }),
|
||||
run: h('button', { class: 'btn btn--primary btn--big', type: 'button', onclick: () => run(lastState) }),
|
||||
log: h('ul', { class: 'log' }),
|
||||
|
||||
+18
-11
@@ -2,7 +2,8 @@
|
||||
|
||||
import { portfolio } from '../engine/investments.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h, icon } from './dom.js';
|
||||
import { h, icon, makeBuyable, setBuyable } from './dom.js';
|
||||
import { bump, floater, pop, purchased } from './fx.js';
|
||||
import { moneyFormatter, setText, signedPercent } from './text.js';
|
||||
|
||||
const AMOUNTS = [0.01, 0.1, 0.5, 1];
|
||||
@@ -20,17 +21,18 @@ export function createInvestView(root, actions) {
|
||||
const change = h('span', { class: 'badge' });
|
||||
const price = h('span');
|
||||
const held = h('span');
|
||||
const buy = h('button', {
|
||||
class: 'btn',
|
||||
type: 'button',
|
||||
text: t('invest.buy'),
|
||||
onclick: () => actions.buy(society.id, amount),
|
||||
});
|
||||
const sell = h('button', {
|
||||
class: 'btn',
|
||||
type: 'button',
|
||||
text: t('invest.sell'),
|
||||
onclick: () => actions.sell(society.id, amount),
|
||||
onclick: (event) => {
|
||||
event.stopPropagation(); // the block itself buys
|
||||
if (actions.sell(society.id, amount)) {
|
||||
pop(root);
|
||||
floater(sell, '↘');
|
||||
bump(held);
|
||||
}
|
||||
},
|
||||
});
|
||||
const root = h(
|
||||
'li',
|
||||
@@ -47,9 +49,14 @@ export function createInvestView(root, actions) {
|
||||
h('p', { class: 'card__meta' }, held),
|
||||
),
|
||||
),
|
||||
h('div', { class: 'card__actions' }, buy, sell),
|
||||
h('div', { class: 'card__actions' }, h('span', { class: 'card__hint', text: t('invest.clickToBuy') }), sell),
|
||||
);
|
||||
return { root, change, price, held, buy, sell };
|
||||
makeBuyable(root, () => {
|
||||
if (!actions.buy(society.id, amount)) return;
|
||||
purchased(root, '↗');
|
||||
bump(held);
|
||||
});
|
||||
return { root, change, price, held, sell };
|
||||
}
|
||||
|
||||
function tierPanel(tier) {
|
||||
@@ -172,7 +179,7 @@ export function createInvestView(root, actions) {
|
||||
? t('invest.heldLine', { value: money(value), profit: signedPercent(basis > 0 ? value / basis - 1 : 0, fmt) })
|
||||
: t('invest.heldNone'),
|
||||
);
|
||||
row.buy.disabled = state.funds < 0.01;
|
||||
setBuyable(row.root, state.funds >= 0.01);
|
||||
row.sell.disabled = shares <= 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { legacyGain, perkCost, perkLevel } from '../engine/legacy.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h, icon } from './dom.js';
|
||||
import { h, icon, makeBuyable, setBuyable } from './dom.js';
|
||||
import { bump, purchased } from './fx.js';
|
||||
import { effectText, setText } from './text.js';
|
||||
|
||||
export function createLegacyView(root, actions) {
|
||||
@@ -30,8 +31,7 @@ export function createLegacyView(root, actions) {
|
||||
const perks = h('ul', { class: 'cards' });
|
||||
for (const perk of ctx.legacy) {
|
||||
const level = h('span', { class: 'badge' });
|
||||
const cost = h('span', { class: 'btn__label' });
|
||||
const buy = h('button', { class: 'btn btn--buy', type: 'button', onclick: () => actions.buyPerk(perk.id) }, cost);
|
||||
const cost = h('strong');
|
||||
const li = h(
|
||||
'li',
|
||||
{ class: 'card' },
|
||||
@@ -42,10 +42,15 @@ export function createLegacyView(root, actions) {
|
||||
h('h3', { class: 'card__title' }, h('span', { text: t(`legacy.perk.${perk.id}`) }), level),
|
||||
h('p', { class: 'card__desc', text: effectText(perk.effect, fmt) }),
|
||||
),
|
||||
buy,
|
||||
h('div', { class: 'card__price' }, cost),
|
||||
);
|
||||
makeBuyable(li, () => {
|
||||
if (!actions.buyPerk(perk.id)) return;
|
||||
purchased(li, '+1', null);
|
||||
bump(level);
|
||||
});
|
||||
perks.append(li);
|
||||
perkRows.set(perk.id, { perk, level, cost, buy });
|
||||
perkRows.set(perk.id, { perk, li, level, cost });
|
||||
}
|
||||
|
||||
root.replaceChildren(
|
||||
@@ -68,7 +73,13 @@ export function createLegacyView(root, actions) {
|
||||
els.need,
|
||||
els.restart,
|
||||
),
|
||||
h('section', { class: 'panel' }, h('h2', { class: 'panel__title', text: t('legacy.perks') }), perks),
|
||||
h(
|
||||
'section',
|
||||
{ class: 'panel' },
|
||||
h('h2', { class: 'panel__title', text: t('legacy.perks') }),
|
||||
h('p', { class: 'hint', text: t('ui.clickToBuy') }),
|
||||
perks,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,13 +103,13 @@ export function createLegacyView(root, actions) {
|
||||
setText(els.restart, t('legacy.restart'));
|
||||
els.restart.disabled = gain < 1;
|
||||
|
||||
for (const { perk, level, cost, buy } of perkRows.values()) {
|
||||
for (const { perk, li, level, cost } of perkRows.values()) {
|
||||
const lv = perkLevel(state, perk);
|
||||
const maxed = lv >= perk.maxLevel;
|
||||
const price = perkCost(perk, lv);
|
||||
setText(level, t('legacy.level', { level: lv, max: perk.maxLevel }));
|
||||
setText(cost, maxed ? t('legacy.maxed') : t('legacy.perkCost', { cost: fmt.number(price) }));
|
||||
buy.disabled = maxed || state.legacy.points < price;
|
||||
setBuyable(li, !maxed && state.legacy.points >= price);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { labCost, researchStatus } from '../engine/research.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { h, icon } from './dom.js';
|
||||
import { h, icon, makeBuyable, setBuyable } from './dom.js';
|
||||
import { purchased } from './fx.js';
|
||||
import { effectText, moneyFormatter, setText } from './text.js';
|
||||
|
||||
const BRANCHES = ['prod', 'mkt', 'log', 'fin', 'ins'];
|
||||
@@ -16,11 +17,8 @@ export function createResearchView(root, actions) {
|
||||
|
||||
function nodeRow(node) {
|
||||
const status = h('p', { class: 'card__meta' });
|
||||
const buy = h('button', {
|
||||
class: 'btn btn--buy',
|
||||
type: 'button',
|
||||
onclick: () => actions.buyNode(node.id),
|
||||
});
|
||||
const cost = h('strong');
|
||||
const price = h('div', { class: 'card__price' }, cost);
|
||||
const li = h(
|
||||
'li',
|
||||
{ class: 'card' },
|
||||
@@ -32,9 +30,12 @@ export function createResearchView(root, actions) {
|
||||
h('p', { class: 'card__desc', text: effectText(node.effect, fmt) }),
|
||||
status,
|
||||
),
|
||||
buy,
|
||||
price,
|
||||
);
|
||||
return { li, status, buy, node };
|
||||
makeBuyable(li, () => {
|
||||
if (actions.buyNode(node.id)) purchased(li, '✓', null);
|
||||
});
|
||||
return { li, status, price, cost, node };
|
||||
}
|
||||
|
||||
function rebuild(newCtx, formatter) {
|
||||
@@ -47,7 +48,13 @@ export function createResearchView(root, actions) {
|
||||
rp: h('strong'),
|
||||
rate: h('span', { class: 'hint' }),
|
||||
labs: h('span', { class: 'hint' }),
|
||||
buyLab: h('button', { class: 'btn btn--wide', type: 'button', onclick: () => actions.buyLab() }),
|
||||
buyLab: h('button', {
|
||||
class: 'btn btn--wide',
|
||||
type: 'button',
|
||||
onclick: () => {
|
||||
if (actions.buyLab()) purchased(els.buyLab, '+1');
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const branchPanels = BRANCHES.map((branch) => {
|
||||
@@ -70,7 +77,7 @@ export function createResearchView(root, actions) {
|
||||
'section',
|
||||
{ class: 'panel panel--wide' },
|
||||
h('h2', { class: 'panel__title', text: t('tab.research') }),
|
||||
h('p', { class: 'hint', text: t('research.intro') }),
|
||||
h('p', { class: 'hint', text: `${t('research.intro')} ${t('ui.clickToBuy')}` }),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'tile' },
|
||||
@@ -95,13 +102,13 @@ export function createResearchView(root, actions) {
|
||||
setText(els.buyLab, maxed ? t('research.labMax') : t('research.buyLab', { cost: money(cost) }));
|
||||
els.buyLab.disabled = maxed || state.funds < cost;
|
||||
|
||||
for (const { li, status, buy, node } of nodeRows.values()) {
|
||||
for (const { li, status, price, cost, node } of nodeRows.values()) {
|
||||
const s = researchStatus(state, node);
|
||||
li.classList.toggle('card--done', s === 'owned');
|
||||
li.classList.toggle('card--locked', s === 'locked');
|
||||
buy.hidden = s !== 'available';
|
||||
buy.disabled = state.rp < node.cost;
|
||||
setText(buy, t('research.cost', { cost: fmt.number(node.cost) }));
|
||||
price.hidden = s !== 'available';
|
||||
setText(cost, t('research.cost', { cost: fmt.number(node.cost) }));
|
||||
setBuyable(li, s === 'available' && state.rp >= node.cost);
|
||||
if (s === 'owned') setText(status, t('research.owned'));
|
||||
else if (s === 'locked') {
|
||||
const names = node.requires.map((id) => t(`research.node.${id}`)).join(', ');
|
||||
|
||||
+46
-29
@@ -3,16 +3,14 @@
|
||||
|
||||
import { generatorQuote, isUnlocked, materialPurchase, upgradeVisible } from '../engine/engine.js';
|
||||
import { t } from '../services/i18n.js';
|
||||
import { $, h, icon, refreshStaticIcons } from './dom.js';
|
||||
import { $, h, icon, makeBuyable, refreshStaticIcons, setBuyable } from './dom.js';
|
||||
import { bump, floater, pop, purchased } from './fx.js';
|
||||
import { effectText, setText } from './text.js';
|
||||
|
||||
const SLIDER_STEPS = 1000;
|
||||
const MARKET_TOLERANCE = 1.15; // demand within ±15 % of output counts as balanced
|
||||
const TREND = { '-1': '▼', 1: '▲' }; // last move of the material price
|
||||
|
||||
function setText(node, text) {
|
||||
if (node.textContent !== text) node.textContent = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param actions callbacks into the game: make, buyMaterial, buyMarketing,
|
||||
* buyGenerator(id, amount), buyUpgrade(id), setPrice(value)
|
||||
@@ -44,6 +42,7 @@ export function createView(actions) {
|
||||
let ctx;
|
||||
let fmt;
|
||||
let buyAmount = 1;
|
||||
let current = null; // the state and derived values of the last refresh
|
||||
let generatorRows = new Map();
|
||||
let upgradeRows = new Map();
|
||||
let upgradeSignature = null;
|
||||
@@ -53,9 +52,22 @@ export function createView(actions) {
|
||||
|
||||
// ---------------------------------------------------------------- controls
|
||||
|
||||
el.make.addEventListener('click', () => actions.make());
|
||||
el.buyMaterial.addEventListener('click', () => actions.buyMaterial());
|
||||
el.buyMarketing.addEventListener('click', () => actions.buyMarketing());
|
||||
el.make.addEventListener('click', (event) => {
|
||||
if (!actions.make()) return;
|
||||
pop(el.make);
|
||||
// The "+N" rises from the pointer (or from the button when used with the keyboard).
|
||||
const at = event.clientX || event.clientY ? { x: event.clientX, y: event.clientY } : undefined;
|
||||
floater(el.make, `+${fmt.number(current.d.clickYield)}`, at);
|
||||
});
|
||||
el.buyMaterial.addEventListener('click', () => {
|
||||
if (actions.buyMaterial()) purchased(el.buyMaterial, '+');
|
||||
});
|
||||
el.buyMarketing.addEventListener('click', () => {
|
||||
if (actions.buyMarketing()) {
|
||||
purchased(el.buyMarketing, '+1');
|
||||
bump(el.buyMarketingInfo);
|
||||
}
|
||||
});
|
||||
|
||||
const priceFromSlider = () => {
|
||||
const { minPrice, maxPrice } = ctx.rules.market;
|
||||
@@ -78,24 +90,17 @@ export function createView(actions) {
|
||||
|
||||
// ------------------------------------------------------------------- lists
|
||||
|
||||
function effectText(effect) {
|
||||
const params = { value: fmt.number(effect.value) };
|
||||
if (effect.target) params.target = t(`theme.gen.${effect.target}.name`);
|
||||
if (effect.type === 'batch_discount') params.percent = fmt.number(Math.round((1 - effect.value) * 100));
|
||||
return t(`effect.${effect.type}`, params);
|
||||
/** What a click on a generator block buys right now (at least one). */
|
||||
function shownQuote(id) {
|
||||
const quote = generatorQuote(current.state, ctx, id, buyAmount);
|
||||
return quote.count > 0 ? quote : generatorQuote(current.state, ctx, id, 1);
|
||||
}
|
||||
|
||||
function generatorRow(g) {
|
||||
const owned = h('span', { class: 'badge' });
|
||||
const rate = h('span');
|
||||
const amount = h('span', { class: 'btn__label' });
|
||||
const amount = h('strong');
|
||||
const cost = h('small');
|
||||
const buy = h(
|
||||
'button',
|
||||
{ class: 'btn btn--buy', type: 'button', onclick: () => actions.buyGenerator(g.id, buyAmount) },
|
||||
amount,
|
||||
cost,
|
||||
);
|
||||
const root = h(
|
||||
'li',
|
||||
{ class: 'card', hidden: true },
|
||||
@@ -107,14 +112,20 @@ export function createView(actions) {
|
||||
h('p', { class: 'card__desc', text: t(`theme.gen.${g.id}.desc`) }),
|
||||
h('p', { class: 'card__meta' }, rate),
|
||||
),
|
||||
buy,
|
||||
h('div', { class: 'card__price' }, amount, cost),
|
||||
);
|
||||
makeBuyable(root, () => {
|
||||
const { count } = shownQuote(g.id);
|
||||
if (!actions.buyGenerator(g.id, buyAmount)) return;
|
||||
purchased(root, `+${fmt.number(count)}`);
|
||||
bump(owned);
|
||||
});
|
||||
el.generators.append(root);
|
||||
return { root, owned, rate, amount, cost, buy };
|
||||
return { root, owned, rate, amount, cost };
|
||||
}
|
||||
|
||||
function upgradeRow(u) {
|
||||
const cost = h('small');
|
||||
const cost = h('strong');
|
||||
const root = h(
|
||||
'li',
|
||||
{ class: 'card' },
|
||||
@@ -123,11 +134,16 @@ export function createView(actions) {
|
||||
'div',
|
||||
{ class: 'card__body' },
|
||||
h('h3', { class: 'card__title', text: t(`theme.upg.${u.id}`) }),
|
||||
h('p', { class: 'card__desc', text: effectText(u.effect) }),
|
||||
h('p', { class: 'card__desc', text: effectText(u.effect, fmt) }),
|
||||
),
|
||||
h('button', { class: 'btn btn--buy', type: 'button', onclick: () => actions.buyUpgrade(u.id) }, cost),
|
||||
h('div', { class: 'card__price' }, cost),
|
||||
);
|
||||
return { root, cost, buy: root.lastElementChild, upgrade: u };
|
||||
makeBuyable(root, () => {
|
||||
if (!actions.buyUpgrade(u.id)) return;
|
||||
purchased(root, '✓');
|
||||
bump(el.upgradeCount);
|
||||
});
|
||||
return { root, cost, upgrade: u };
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ public
|
||||
@@ -160,9 +176,9 @@ export function createView(actions) {
|
||||
|
||||
setText(row.owned, t('ui.owned', { count: fmt.number(owned) }));
|
||||
setText(row.rate, t('ui.rateEach', { rate: fmt.number(d.perGenerator[g.id]) }));
|
||||
setText(row.amount, t('ui.buyN', { count: fmt.number(quote.count) }));
|
||||
setText(row.amount, `×${fmt.number(quote.count)}`);
|
||||
setText(row.cost, money(quote.cost));
|
||||
row.buy.disabled = state.funds < quote.cost;
|
||||
setBuyable(row.root, state.funds >= quote.cost);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +193,7 @@ export function createView(actions) {
|
||||
}
|
||||
for (const row of upgradeRows.values()) {
|
||||
setText(row.cost, money(row.upgrade.cost));
|
||||
row.buy.disabled = state.funds < row.upgrade.cost;
|
||||
setBuyable(row.root, state.funds >= row.upgrade.cost);
|
||||
}
|
||||
setText(
|
||||
el.upgradeCount,
|
||||
@@ -222,6 +238,7 @@ export function createView(actions) {
|
||||
|
||||
/** @param flow { income } smoothed funds per second */
|
||||
function update(state, d, flow) {
|
||||
current = { state, d };
|
||||
setText(el.units, fmt.number(Math.floor(state.units)));
|
||||
setText(el.funds, money(state.funds));
|
||||
setText(el.material, fmt.number(Math.floor(state.material)));
|
||||
|
||||
@@ -29,14 +29,14 @@
|
||||
"marketingInfo": "Level {level} → demand ×{multiplier} · {cost}",
|
||||
"owned": "Owned: {count}",
|
||||
"rateEach": "{rate}/s each",
|
||||
"buyN": "Buy ×{count}",
|
||||
"upgradeCount": "{owned} / {total}",
|
||||
"hintDemand": "At this price customers want {demand}/s; you make {production}/s.",
|
||||
"hintDemandOnly": "At this price customers want {demand}/s.",
|
||||
"marketShort": "Demand outpaces your output: you are selling too cheap.",
|
||||
"marketLong": "You make more than customers want: stock piles up. Try a lower price.",
|
||||
"marketBalanced": "Supply and demand are about even.",
|
||||
"menu": "Menu"
|
||||
"menu": "Menu",
|
||||
"clickToBuy": "Click a block to buy it."
|
||||
},
|
||||
"effect": {
|
||||
"click_mult": "Manual work ×{value}",
|
||||
@@ -91,7 +91,6 @@
|
||||
"tabUnlocked": "New section unlocked: {name}",
|
||||
"contractDone": "Contract complete! {reward}",
|
||||
"contractFailed": "Contract failed: time ran out.",
|
||||
"eventOpportunity": "Opportunity: {title}! Seize it in the Contracts tab.",
|
||||
"eventIncident": "Incident: {title}",
|
||||
"achievement": "Achievement: {title}",
|
||||
"restarted": "A new run begins! +{gain} {@theme.res.legacy}"
|
||||
@@ -125,11 +124,11 @@
|
||||
"amountLabel": "Amount per trade (share of your funds when buying, of the holding when selling)",
|
||||
"amount": "Amount per trade",
|
||||
"nextUpdate": "Next price update in {seconds} s",
|
||||
"buy": "Buy",
|
||||
"sell": "Sell",
|
||||
"priceLine": "Price {price} ({gap} vs fair value)",
|
||||
"heldLine": "Held: {value} ({profit})",
|
||||
"heldNone": "Nothing held"
|
||||
"heldNone": "Nothing held",
|
||||
"clickToBuy": "Click to buy"
|
||||
},
|
||||
"gamble": {
|
||||
"intro": "{@theme.res.chips} are hard to buy and harder to keep. Stake some on a cycle: win and the stake doubles, hit a jackpot and it multiplies, lose and it is gone, crash and you lose even more. They also boost demand.",
|
||||
@@ -311,7 +310,6 @@
|
||||
"perLevel": "Each level: {effect}",
|
||||
"total": "Now: {effect}",
|
||||
"none": "No level yet",
|
||||
"buy": "Buy ×{count}",
|
||||
"maxed": "Maxed",
|
||||
"name": {
|
||||
"generator": "{target} overhaul",
|
||||
|
||||
@@ -29,14 +29,14 @@
|
||||
"marketingInfo": "Niveau {level} → demande ×{multiplier} · {cost}",
|
||||
"owned": "Possédé : {count}",
|
||||
"rateEach": "{rate}/s chacun",
|
||||
"buyN": "Acheter ×{count}",
|
||||
"upgradeCount": "{owned} / {total}",
|
||||
"hintDemand": "À ce prix, les clients veulent {demand}/s ; vous produisez {production}/s.",
|
||||
"hintDemandOnly": "À ce prix, les clients veulent {demand}/s.",
|
||||
"marketShort": "La demande dépasse votre production : vous vendez trop bon marché.",
|
||||
"marketLong": "Vous produisez plus que la demande : le stock s'accumule. Essayez un prix plus bas.",
|
||||
"marketBalanced": "L'offre et la demande sont à peu près égales.",
|
||||
"menu": "Menu"
|
||||
"menu": "Menu",
|
||||
"clickToBuy": "Cliquez sur un bloc pour l'acheter."
|
||||
},
|
||||
"effect": {
|
||||
"click_mult": "Travail manuel ×{value}",
|
||||
@@ -91,7 +91,6 @@
|
||||
"tabUnlocked": "Nouvelle section débloquée : {name}",
|
||||
"contractDone": "Contrat terminé ! {reward}",
|
||||
"contractFailed": "Contrat échoué : le temps est écoulé.",
|
||||
"eventOpportunity": "Opportunité : {title} ! Saisissez-la dans l'onglet Contrats.",
|
||||
"eventIncident": "Incident : {title}",
|
||||
"achievement": "Succès : {title}",
|
||||
"restarted": "Une nouvelle partie commence ! +{gain} ({@theme.res.legacy})"
|
||||
@@ -125,11 +124,11 @@
|
||||
"amountLabel": "Montant par transaction (part de vos fonds à l'achat, du titre détenu à la vente)",
|
||||
"amount": "Montant par transaction",
|
||||
"nextUpdate": "Prochaine mise à jour des prix dans {seconds} s",
|
||||
"buy": "Acheter",
|
||||
"sell": "Vendre",
|
||||
"priceLine": "Prix {price} ({gap} par rapport à la valeur juste)",
|
||||
"heldLine": "Détenu : {value} ({profit})",
|
||||
"heldNone": "Rien de détenu"
|
||||
"heldNone": "Rien de détenu",
|
||||
"clickToBuy": "Cliquer pour acheter"
|
||||
},
|
||||
"gamble": {
|
||||
"intro": "{@theme.res.chips} : difficiles à acheter, plus difficiles encore à garder. Misez-en sur un cycle : gagnez et la mise double, tirez le jackpot et elle se multiplie, perdez et elle disparaît, plantez-vous et vous perdez encore plus. Ils augmentent aussi la demande.",
|
||||
@@ -311,7 +310,6 @@
|
||||
"perLevel": "Chaque niveau : {effect}",
|
||||
"total": "Actuellement : {effect}",
|
||||
"none": "Aucun niveau pour l'instant",
|
||||
"buy": "Acheter ×{count}",
|
||||
"maxed": "Au maximum",
|
||||
"name": {
|
||||
"generator": "Refonte : {target}",
|
||||
|
||||
+43
-56
@@ -35,10 +35,8 @@ function render(dict, key, params = {}, depth = 0) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups of icons. `themed` groups have a different subject in every theme
|
||||
* (so the table has one column per theme); the others have the same subject
|
||||
* everywhere (but still need drawing in each theme's style).
|
||||
* Each icon's `label(dict)` gives what to draw, from that theme's texts.
|
||||
* Groups of icons. Each icon's `label(dict)` says what to draw, from that
|
||||
* theme's texts. Every group has to be drawn in every theme's style.
|
||||
*/
|
||||
export function iconGroups(ctx) {
|
||||
const text = (key) => (dict) => render(dict, key);
|
||||
@@ -46,38 +44,23 @@ export function iconGroups(ctx) {
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'res',
|
||||
title: 'Resources',
|
||||
shownIn: 'top bar (units, funds, material), Gamble, Research and Legacy screens',
|
||||
themed: true,
|
||||
icons: ['units', 'funds', 'material', 'chips', 'research', 'legacy'].map(resource),
|
||||
},
|
||||
{
|
||||
id: 'gen',
|
||||
title: 'Automation tiers',
|
||||
shownIn: 'Production screen, Automation list',
|
||||
themed: true,
|
||||
icons: ctx.generators.map((g) => ({ key: `gen.${g.id}`, label: text(`theme.gen.${g.id}.name`) })),
|
||||
},
|
||||
{
|
||||
id: 'upg',
|
||||
title: 'Upgrades',
|
||||
shownIn: 'Production screen, Upgrades list',
|
||||
themed: true,
|
||||
icons: ctx.upgrades.map((u) => ({ key: `upg.${u.id}`, label: text(`theme.upg.${u.id}`) })),
|
||||
},
|
||||
{
|
||||
id: 'inv',
|
||||
title: 'Investment societies',
|
||||
shownIn: 'Investments screen',
|
||||
themed: true,
|
||||
icons: ctx.investments.societies.map((s) => ({ key: `inv.${s.id}`, label: text(`theme.inv.${s.id}.name`) })),
|
||||
},
|
||||
{
|
||||
id: 'endless',
|
||||
title: 'Endless upgrades',
|
||||
shownIn: 'Production screen, Endless upgrades panel',
|
||||
themed: true,
|
||||
icons: ctx.endless.map((line) => ({
|
||||
key: `endless.${line.id}`,
|
||||
label: (dict) =>
|
||||
@@ -87,25 +70,24 @@ export function iconGroups(ctx) {
|
||||
})),
|
||||
},
|
||||
{
|
||||
id: 'research',
|
||||
title: 'Investment societies',
|
||||
shownIn: 'Investments screen',
|
||||
icons: ctx.investments.societies.map((s) => ({ key: `inv.${s.id}`, label: text(`theme.inv.${s.id}.name`) })),
|
||||
},
|
||||
{
|
||||
title: 'Research',
|
||||
shownIn: 'Research screen',
|
||||
themed: false,
|
||||
icons: ctx.research.map((r) => ({ key: `research.${r.id}`, label: text(`research.node.${r.id}`) })),
|
||||
},
|
||||
{
|
||||
id: 'legacy',
|
||||
title: 'Legacy perks',
|
||||
shownIn: 'Legacy screen',
|
||||
themed: false,
|
||||
icons: ctx.legacy.map((p) => ({ key: `legacy.${p.id}`, label: text(`legacy.perk.${p.id}`) })),
|
||||
},
|
||||
{
|
||||
id: 'ach',
|
||||
title: 'Achievements',
|
||||
shownIn: 'Achievements screen',
|
||||
themed: false,
|
||||
icons: ctx.achievements.map((a) => ({ key: `ach.${a.id}`, label: text(`ach.title.${a.id}`) })),
|
||||
title: 'Trophy',
|
||||
shownIn: 'Achievements screen (one shared icon for all achievements)',
|
||||
icons: [{ key: 'trophy', label: () => 'A trophy' }],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -114,46 +96,51 @@ export function allIconKeys(ctx) {
|
||||
return iconGroups(ctx).flatMap((group) => group.icons.map((icon) => icon.key));
|
||||
}
|
||||
|
||||
/** Markdown for the README: totals, then one table per group. */
|
||||
export function renderMarkdown(ctx) {
|
||||
/** Default file name of an icon: gen.gen1 -> img/gen-gen1.svg */
|
||||
export function fileFor(key, extension = 'svg') {
|
||||
return `img/${key.replaceAll('.', '-')}.${extension}`;
|
||||
}
|
||||
|
||||
/** The plain-text work list written to ICONS.txt: one checklist per theme. */
|
||||
export function renderText(ctx) {
|
||||
const groups = iconGroups(ctx);
|
||||
const dicts = Object.fromEntries(THEME_IDS.map((id) => [id, dictionaryFor(id)]));
|
||||
const perTheme = groups.reduce((sum, g) => sum + g.icons.length, 0);
|
||||
const themeColumns = THEME_IDS.map((id) => `\`${id}\``);
|
||||
const total = groups.reduce((sum, g) => sum + g.icons.length, 0);
|
||||
const all = groups.flatMap((g) => g.icons);
|
||||
const keyWidth = Math.max(...all.map((icon) => icon.key.length));
|
||||
const fileWidth = Math.max(...all.map((icon) => fileFor(icon.key).length));
|
||||
|
||||
const lines = [
|
||||
`**${perTheme} icons per theme, ${perTheme * THEME_IDS.length} in total** (${THEME_IDS.length} themes: ${themeColumns.join(', ')}).`,
|
||||
'ICONS TO PRODUCE',
|
||||
'================',
|
||||
'',
|
||||
'| Group | Icons per theme | Shown in |',
|
||||
'|---|---:|---|',
|
||||
...groups.map((g) => `| ${g.title} | ${g.icons.length} | ${g.shownIn} |`),
|
||||
`| **Total** | **${perTheme}** | |`,
|
||||
'Generated by "npm run icons -- --write" from the game data: do not edit by hand.',
|
||||
`${total} icons per theme, ${total * THEME_IDS.length} in total (${THEME_IDS.length} themes: ${THEME_IDS.join(', ')}).`,
|
||||
'',
|
||||
'Format One square image per icon: SVG preferred (or PNG, 128x128 px or more),',
|
||||
' transparent background, readable at 28 px (top bar) and 48 px (lists).',
|
||||
"Files public/themes/<theme>/img/<file>, then listed in that theme's theme.json:",
|
||||
' { "assets": { "<key>": "img/<file>" } } (an icon that is not listed keeps its grey square)',
|
||||
'',
|
||||
'SUMMARY (icons per theme)',
|
||||
...groups.map((g) => ` ${String(g.icons.length).padStart(3)} ${g.title} [${g.shownIn}]`),
|
||||
` ${String(total).padStart(3)} TOTAL`,
|
||||
];
|
||||
|
||||
for (const themeId of THEME_IDS) {
|
||||
const dict = dictionaryFor(themeId);
|
||||
lines.push('', '', `THEME: ${themeId}`, '-'.repeat(7 + themeId.length));
|
||||
for (const group of groups) {
|
||||
lines.push(`#### ${group.title} (${group.icons.length})`, '');
|
||||
if (group.themed) {
|
||||
lines.push(
|
||||
`| Key | ${themeColumns.join(' | ')} |`,
|
||||
`|---|${THEME_IDS.map(() => '---').join('|')}|`,
|
||||
...group.icons.map(
|
||||
(icon) => `| \`${icon.key}\` | ${THEME_IDS.map((id) => icon.label(dicts[id])).join(' | ')} |`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
"| Key | Subject (the same in every theme, drawn in each theme's style) |",
|
||||
'|---|---|',
|
||||
...group.icons.map((icon) => `| \`${icon.key}\` | ${icon.label(dicts[THEME_IDS[0]])} |`),
|
||||
);
|
||||
lines.push('', `${group.title} (${group.icons.length})`);
|
||||
for (const icon of group.icons) {
|
||||
const file = fileFor(icon.key).padEnd(fileWidth);
|
||||
lines.push(` [ ] ${file} ${icon.key.padEnd(keyWidth)} ${icon.label(dict)}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
return lines.join('\n').trimEnd();
|
||||
}
|
||||
return `${lines.join('\n')}\n`;
|
||||
}
|
||||
|
||||
/** theme.json "assets" skeleton: every key mapped to its default file name. */
|
||||
export function assetsSkeleton(ctx, extension = 'svg') {
|
||||
return Object.fromEntries(allIconKeys(ctx).map((key) => [key, `img/${key.replaceAll('.', '-')}.${extension}`]));
|
||||
return Object.fromEntries(allIconKeys(ctx).map((key) => [key, fileFor(key, extension)]));
|
||||
}
|
||||
|
||||
+8
-18
@@ -1,35 +1,25 @@
|
||||
// Icon list tooling.
|
||||
//
|
||||
// npm run icons print the exhaustive list as Markdown
|
||||
// npm run icons -- --write refresh the list inside README.md
|
||||
// npm run icons print the work list
|
||||
// npm run icons -- --write write it to ICONS.txt
|
||||
// npm run icons -- --manifest print a theme.json "assets" skeleton (every key -> img/<key>.svg)
|
||||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { assetsSkeleton, renderMarkdown } from './icon-keys.js';
|
||||
import { loadContext } from './context.js';
|
||||
import { assetsSkeleton, renderText } from './icon-keys.js';
|
||||
|
||||
export const README = fileURLToPath(new URL('../README.md', import.meta.url));
|
||||
export const START = '<!-- icons:start (generated by `npm run icons -- --write`, do not edit by hand) -->';
|
||||
export const END = '<!-- icons:end -->';
|
||||
|
||||
/** README text with the generated list refreshed between the markers. */
|
||||
export function withIconList(readme, ctx) {
|
||||
const start = readme.indexOf(START);
|
||||
const end = readme.indexOf(END);
|
||||
if (start < 0 || end < start) throw new Error('README.md has no icon list markers');
|
||||
return `${readme.slice(0, start + START.length)}\n\n${renderMarkdown(ctx)}\n\n${readme.slice(end)}`;
|
||||
}
|
||||
export const ICONS_FILE = fileURLToPath(new URL('../ICONS.txt', import.meta.url));
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const ctx = loadContext();
|
||||
const args = process.argv.slice(2);
|
||||
if (args.includes('--write')) {
|
||||
writeFileSync(README, withIconList(readFileSync(README, 'utf8'), ctx));
|
||||
console.log('README.md updated');
|
||||
writeFileSync(ICONS_FILE, renderText(ctx));
|
||||
console.log('ICONS.txt updated');
|
||||
} else if (args.includes('--manifest')) {
|
||||
console.log(JSON.stringify({ assets: assetsSkeleton(ctx) }, null, 2));
|
||||
} else {
|
||||
console.log(renderMarkdown(ctx));
|
||||
process.stdout.write(renderText(ctx));
|
||||
}
|
||||
}
|
||||
|
||||
+42
-31
@@ -1,66 +1,77 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { test } from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { ICONS_FILE } from '../scripts/icons.mjs';
|
||||
import { allIconKeys, assetsSkeleton, fileFor, renderText, THEME_IDS } from '../scripts/icon-keys.js';
|
||||
import { loadContext, PUBLIC_DIR } from './helpers.js';
|
||||
import { README, withIconList } from '../scripts/icons.mjs';
|
||||
import { allIconKeys, assetsSkeleton, iconGroups, THEME_IDS } from '../scripts/icon-keys.js';
|
||||
|
||||
const ctx = loadContext();
|
||||
const normalise = (text) => text.replaceAll('\r\n', '\n');
|
||||
|
||||
test('the icon list in the README is up to date (run: npm run icons -- --write)', () => {
|
||||
const readme = readFileSync(README, 'utf8').replaceAll('\r\n', '\n');
|
||||
assert.equal(readme, withIconList(readme, ctx));
|
||||
test('ICONS.txt is up to date (run: npm run icons -- --write)', () => {
|
||||
assert.equal(normalise(readFileSync(ICONS_FILE, 'utf8')), renderText(ctx));
|
||||
});
|
||||
|
||||
test('the README lists every single icon key, once per theme row', () => {
|
||||
const readme = readFileSync(README, 'utf8');
|
||||
test('the README points to ICONS.txt instead of embedding the list', () => {
|
||||
const readme = readFileSync(new URL('../README.md', import.meta.url), 'utf8');
|
||||
assert.ok(readme.includes('ICONS.txt'));
|
||||
assert.ok(!readme.includes('icons:start'), 'no generated block in the README');
|
||||
assert.ok(readme.split('\n').length < 200, 'the README stays light');
|
||||
});
|
||||
|
||||
test('ICONS.txt lists every icon, once per theme, with its file name', () => {
|
||||
const text = normalise(readFileSync(ICONS_FILE, 'utf8'));
|
||||
const keys = allIconKeys(ctx);
|
||||
assert.equal(new Set(keys).size, keys.length, 'icon keys are unique');
|
||||
for (const key of keys) assert.ok(readme.includes(`| \`${key}\` |`), key);
|
||||
for (const theme of THEME_IDS) {
|
||||
const section = text.slice(text.indexOf(`THEME: ${theme}\n`));
|
||||
const end = section.indexOf('\n\n\nTHEME: ', 1);
|
||||
const body = end < 0 ? section : section.slice(0, end);
|
||||
for (const key of keys) {
|
||||
assert.ok(body.includes(` ${key} `) && body.includes(fileFor(key)), `${theme}: ${key}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('every theme names every themed icon (nothing falls back to a raw key)', () => {
|
||||
const full = readFileSync(README, 'utf8');
|
||||
const generated = full.slice(full.indexOf('<!-- icons:start'), full.indexOf('<!-- icons:end'));
|
||||
assert.ok(generated.length > 5000, 'the generated list is there');
|
||||
assert.ok(!/\| [a-z]+(\.[a-z_0-9]+)+ \|/.test(generated.replace(/`[^`]*`/g, '')), 'no unresolved text key in a table');
|
||||
assert.ok(!generated.includes('{@'), 'embedded theme words are resolved');
|
||||
assert.ok(!/\{target\}|\{value\}/.test(generated), 'placeholders are resolved');
|
||||
test('every achievement shares one trophy icon', () => {
|
||||
assert.ok(allIconKeys(ctx).includes('trophy'));
|
||||
assert.ok(!allIconKeys(ctx).some((key) => key.startsWith('ach.')), 'no per-achievement icons');
|
||||
const view = readFileSync(`${PUBLIC_DIR}js/ui/achievementsView.js`, 'utf8');
|
||||
assert.ok(view.includes("icon('trophy')"));
|
||||
});
|
||||
|
||||
test('nothing in the list is left as a raw text key or placeholder', () => {
|
||||
const text = readFileSync(ICONS_FILE, 'utf8');
|
||||
assert.ok(!text.includes('{@') && !/\{target\}|\{value\}/.test(text));
|
||||
assert.ok(!/ (theme|research|legacy|ach|endless)\.[\w.]+\s*$/m.test(text), 'every label resolved');
|
||||
assert.ok(THEME_IDS.length >= 5);
|
||||
});
|
||||
|
||||
test('the interface only uses icon keys that are documented', () => {
|
||||
const documented = new Set(iconGroups(ctx).map((group) => group.id));
|
||||
test('the interface only uses icon keys that are listed', () => {
|
||||
const listed = new Set(allIconKeys(ctx).map((key) => key.split('.')[0]));
|
||||
const used = new Set();
|
||||
const walk = (dir) => {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = `${dir}/${entry.name}`;
|
||||
if (entry.isDirectory()) walk(path);
|
||||
else if (entry.name.endsWith('.js')) {
|
||||
// icon('gen.x') and icon(`inv.${id}`) calls, outside the helper's own definition
|
||||
if (path.endsWith('ui/dom.js')) continue;
|
||||
for (const m of readFileSync(path, 'utf8').matchAll(/\bicon\(\s*[`'"]([a-z]+)\./g)) used.add(m[1]);
|
||||
else if (entry.name.endsWith('.js') && !path.endsWith('ui/dom.js')) {
|
||||
// icon('trophy'), icon(`inv.${id}`) ...: the first segment of the key
|
||||
for (const m of readFileSync(path, 'utf8').matchAll(/\bicon\(\s*[`'"]([a-z]+)/g)) used.add(m[1]);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(`${PUBLIC_DIR}js`);
|
||||
for (const m of readFileSync(`${PUBLIC_DIR}index.html`, 'utf8').matchAll(/data-icon="([a-z]+)\./g)) used.add(m[1]);
|
||||
for (const m of readFileSync(`${PUBLIC_DIR}index.html`, 'utf8').matchAll(/data-icon="([a-z]+)/g)) used.add(m[1]);
|
||||
|
||||
assert.ok(used.size >= 7, 'the scan found the icons');
|
||||
const undocumented = [...used].filter((prefix) => !documented.has(prefix));
|
||||
assert.deepEqual(undocumented, [], `icons used but not listed in the README: ${undocumented.join(', ')}`);
|
||||
const unlisted = [...used].filter((prefix) => !listed.has(prefix));
|
||||
assert.deepEqual(unlisted, [], `icons used but not in ICONS.txt: ${unlisted.join(', ')}`);
|
||||
});
|
||||
|
||||
test('the generated manifest covers every key with a real file name', () => {
|
||||
const assets = assetsSkeleton(ctx);
|
||||
assert.deepEqual(Object.keys(assets), allIconKeys(ctx));
|
||||
assert.equal(assets['gen.gen1'], 'img/gen-gen1.svg');
|
||||
assert.equal(assets.trophy, 'img/trophy.svg');
|
||||
assert.ok(Object.values(assets).every((file) => /^img\/[a-z0-9_-]+\.svg$/.test(file)));
|
||||
});
|
||||
|
||||
test('the paths the game reads from theme.json are the same keys', () => {
|
||||
// dom.js looks assets up by the exact key, so a skeleton entry must resolve as-is.
|
||||
const domSource = readFileSync(fileURLToPath(new URL('../public/js/ui/dom.js', import.meta.url)), 'utf8');
|
||||
assert.ok(domSource.includes('assets[key]'));
|
||||
});
|
||||
|
||||
@@ -396,7 +396,7 @@ test('events: an opportunity appears, can be claimed once, and its buff expires'
|
||||
const d = derive(s, ctx);
|
||||
stepEvents(s, ctx, 1, d, constant(0), false, totals); // first weighted type: surge
|
||||
assert.equal(s.events.current.type, 'surge');
|
||||
assert.ok(totals.notices.some((n) => n.kind === 'event_opportunity'));
|
||||
assert.equal(totals.notices.length, 0, 'an opportunity is shown on its card and tab, not as a pop-up');
|
||||
|
||||
const demandBefore = derive(s, ctx).demandMultiplier;
|
||||
assert.ok(claimEvent(s, ctx, derive(s, ctx)));
|
||||
|
||||
Reference in New Issue
Block a user