Skill v1.0.1
Automated scan100/1001 files
version: "1.0.1" name: godot-genre-simulation description: "Expert blueprint for simulation and tycoon games (SimCity, RollerCoaster Tycoon, Factorio, Two Point Hospital) covering economy management, time progression, interconnected systems, NPC simulation, and feedback loops. Use when building management sims, tycoon games, city builders, or resource optimization games. Keywords tycoon, economy system, resource management, time scale, feedback loop, progression unlock, simulation tick."
Genre: Simulation / Tycoon
Optimization, systems mastery, and satisfying feedback loops define management games.
NEVER Do (Expert Anti-Patterns)
Simulation & Economy
- NEVER use floating-point for primary currency; strictly use Integer Cents (or fixed-point math) to prevent accumulated precision errors in financial models.
- NEVER process 1000+ entities individually in
_process(); strictly use a Tick Manager to batch updates or process entities in rotating pools. - NEVER rely on linear cost scaling; strictly use Exponential Growth (
Base * pow(1.15, Level)) to maintain challenge and strategic tension. - NEVER hide critical metrics from the player; strictly provide Detailed Breakdowns (Income vs. Expense) so players can make optimization-based decisions.
- NEVER allow infinite resource stacking; strictly enforce Logistical Caps (warehouses/silos) to create meaningful space-management gameplay loops.
- NEVER let the early game become a "Waiting Simulator"; strictly Front-Load Decisions and quick early wins to build player momentum.
- NEVER modify a shared Resource directly; strictly use `duplicate()` to avoid unintentionally updating every building of that type.
- NEVER tie simulation logic to the visual framerate; strictly use `_physics_process()` or delta accumulators for deterministic simulation results.
Performance & Threading
- NEVER update UI labels every frame; strictly use Event-Driven Signals to refresh UI ONLY when the underlying data changes.
- NEVER run heavy economic loops synchronously; strictly use WorkerThreadPool to offload complex calculations and prevent UI stutters.
- NEVER store massive resource data as Nodes; strictly use `RefCounted` or Data Resources to avoid the memory/CPU overhead of the SceneTree.
- NEVER ignore `OS.low_processor_usage_mode`; strictly enable it for stationary management screens to save massive CPU/Battery life.
- NEVER manipulate the SceneTree from background threads; strictly use `call_deferred()` for thread-safe UI updates.
- NEVER parse large JSON save files on the main thread; strictly use Threaded Serialization or optimized binary
.resformats. - NEVER use standard equality (==) for needs; strictly use `is_equal_approx()` to prevent floating-point jitter failures in logic gates.
🛠 Expert Components (scripts/)
Original Expert Patterns
- sim_tick_manager.gd - Variable-speed tick system decoupling simulation from rendering.
- tycoon_economy.gd - Multi-resource economic engine with integer-precision currency.
Modular Components
- simulation_patterns.gd - Reusable patterns: AStarGrid2D logistics and low-processor modes.
Economy Design
The heart of any tycoon game is its economy. Key principle: multiple interconnected resources that force trade-offs.
Multi-Resource System
class_name TycoonEconomyextends Nodesignal resource_changed(resource_type: String, amount: float)signal went_bankruptvar resources: Dictionary = {"money": 10000.0,"reputation": 50.0, # 0-100"workers": 0,"materials": 100.0,"energy": 100.0}var resource_caps: Dictionary = {"reputation": 100.0,"workers": 50,"energy": 1000.0}func modify_resource(type: String, amount: float) -> bool:if amount < 0 and resources[type] + amount < 0:if type == "money":went_bankrupt.emit()return false # Can't go negativeresources[type] = clamp(resources[type] + amount,0,resource_caps.get(type, INF))resource_changed.emit(type, resources[type])return true
Income/Expense Tracking
class_name FinancialTrackerextends Nodevar income_sources: Dictionary = {} # source_name: amount_per_tickvar expense_sources: Dictionary = {}signal financial_update(profit: float, income: float, expenses: float)func calculate_tick() -> float:var total_income := 0.0var total_expenses := 0.0for source in income_sources.values():total_income += sourcefor source in expense_sources.values():total_expenses += sourcevar profit := total_income - total_expensesfinancial_update.emit(profit, total_income, total_expenses)return profit
Time System
Simulation games need controllable time:
class_name SimulationTimeextends Nodesignal time_tick(delta_game_hours: float)signal day_changed(day: int)signal speed_changed(new_speed: int)enum Speed { PAUSED, NORMAL, FAST, ULTRA }@export var seconds_per_game_hour := 30.0 # Real secondsvar current_speed := Speed.NORMALvar speed_multipliers := {Speed.PAUSED: 0.0,Speed.NORMAL: 1.0,Speed.FAST: 3.0,Speed.ULTRA: 10.0}var current_hour := 8.0 # Start at 8 AMvar current_day := 1func _process(delta: float) -> void:if current_speed == Speed.PAUSED:returnvar game_delta := (delta / seconds_per_game_hour) * speed_multipliers[current_speed]current_hour += game_deltaif current_hour >= 24.0:current_hour -= 24.0current_day += 1day_changed.emit(current_day)time_tick.emit(game_delta)func set_speed(speed: Speed) -> void:current_speed = speedspeed_changed.emit(speed)
Entity Management
Workers/NPCs
class_name Workerextends Nodeenum State { IDLE, WORKING, RESTING, COMMUTING }@export var wage_per_hour: float = 10.0@export var skill_level: float = 1.0 # Productivity multiplier@export var morale: float = 80.0 # 0-100var current_state := State.IDLEvar assigned_workstation: Workstationfunc update(game_hours: float) -> void:match current_state:State.WORKING:if assigned_workstation:var productivity := skill_level * (morale / 100.0)assigned_workstation.work(game_hours * productivity)morale -= game_hours * 0.5 # Working tires workersState.RESTING:morale = min(100.0, morale + game_hours * 2.0)func calculate_hourly_cost() -> float:return wage_per_hour
Buildings/Facilities
class_name Facilityextends Node3D@export var build_cost: Dictionary # resource_type: amount@export var operating_cost_per_hour: float = 5.0@export var capacity: int = 5@export var output_per_hour: Dictionary # resource_type: amountvar assigned_workers: Array[Worker] = []var is_operational := truevar efficiency := 1.0func calculate_output(game_hours: float) -> Dictionary:if not is_operational or assigned_workers.is_empty():return {}var worker_efficiency := 0.0for worker in assigned_workers:worker_efficiency += worker.skill_level * (worker.morale / 100.0)worker_efficiency /= capacity # Normalize to 0-1var result := {}for resource in output_per_hour:result[resource] = output_per_hour[resource] * game_hours * worker_efficiency * efficiencyreturn result
Customer/Demand System
class_name CustomerSimulationextends Node@export var base_customers_per_hour := 10.0@export var demand_curve: Curve # Hour of day vs demand multipliervar customer_queue: Array[Customer] = []func generate_customers(game_hour: float, delta_hours: float) -> void:var demand_mult := demand_curve.sample(game_hour / 24.0)var reputation_mult := Economy.resources["reputation"] / 50.0 # 100 rep = 2x customersvar customers_to_spawn := base_customers_per_hour * delta_hours * demand_mult * reputation_multfor i in int(customers_to_spawn):spawn_customer()func spawn_customer() -> void:var customer := Customer.new()customer.patience = randf_range(30.0, 120.0) # Seconds before leavingcustomer.spending_budget = randf_range(10.0, 100.0)customer_queue.append(customer)
Feedback Systems
Visual Feedback
# Money flying to bank, resources flowing, etc.class_name ResourceFlowVisualizerextends Nodefunc show_income(amount: float, from: Vector2, to: Vector2) -> void:var coin := coin_scene.instantiate()coin.position = fromadd_child(coin)var tween := create_tween()tween.tween_property(coin, "position", to, 0.5)tween.tween_callback(coin.queue_free)var label := Label.new()label.text = "+$" + str(int(amount))label.position = fromadd_child(label)var label_tween := create_tween()label_tween.tween_property(label, "position:y", label.position.y - 30, 0.5)label_tween.parallel().tween_property(label, "modulate:a", 0.0, 0.5)label_tween.tween_callback(label.queue_free)
Statistics Dashboard
class_name StatsDashboardextends Control@export var graph_history_hours := 24var income_history: Array[float] = []var expense_history: Array[float] = []func record_financial_tick(income: float, expenses: float) -> void:income_history.append(income)expense_history.append(expenses)# Keep last N entrieswhile income_history.size() > graph_history_hours:income_history.pop_front()expense_history.pop_front()queue_redraw()func _draw() -> void:# Draw income/expense graphdraw_line_graph(income_history, Color.GREEN)draw_line_graph(expense_history, Color.RED)
Progression & Unlocks
class_name UnlockSystemextends Nodevar unlocks: Dictionary = {"basic_facility": true,"advanced_facility": false,"marketing": false,"automation": false}var unlock_conditions: Dictionary = {"advanced_facility": {"money_earned": 50000},"marketing": {"reputation": 70},"automation": {"workers_hired": 20}}var progress: Dictionary = {"money_earned": 0.0,"workers_hired": 0}func check_unlocks() -> Array[String]:var newly_unlocked: Array[String] = []for unlock in unlock_conditions:if unlocks[unlock]:continue # Already unlockedvar conditions := unlock_conditions[unlock]var all_met := truefor condition in conditions:if progress.get(condition, 0) < conditions[condition]:all_met = falsebreakif all_met:unlocks[unlock] = truenewly_unlocked.append(unlock)return newly_unlocked
Common Pitfalls
| Pitfall | Solution | |
|---|---|---|
| Economy too easy to break | Extensive balancing, soft caps, diminishing returns | |
| Boring early game | Front-load interesting decisions, quick early progression | |
| Information overload | Progressive disclosure, collapsible UI panels | |
| No clear goals | Milestones, achievements, scenarios | |
| Tedious micromanagement | Automation unlocks, batch operations |
Godot-Specific Tips
- UI: Use
Controlnodes extensively,Treefor lists,GraphEditfor connections - Performance: Process entities in batches, not every frame
- Save/Load: Convert all game state to Dictionary for JSON serialization
- Isometric view: Use
Camera2Dwith orthographic projection
🚀 Elite Technical Implementations (Batch 09)
1. Dependency-Graph Pattern (Production Chains)
Represent complex production chains (e.g., Raw Materials -> Intermediate -> Finished Goods) using nested Resource structures. This allows for deep, recursive data definitions that are fully editable in the Inspector.
# production_recipe.gdclass_name ProductionRecipe extends Resource## Mapping of required ItemResources to their integer quantities@export var required_inputs: Dictionary[ItemResource, int] = {}@export var output_item: ItemResource@export var output_yield: int = 1@export var production_time: float = 5.0## Evaluates if the current inventory meets the graph dependenciesfunc can_produce(available_inventory: Dictionary[ItemResource, int]) -> bool:for input_item in required_inputs:var required_amount: int = required_inputs[input_item]var available_amount: int = available_inventory.get(input_item, 0)if available_amount < required_amount:return falsereturn true
2. AStarGrid2D for Logistics & NPC Jobs
AStarGrid2D is specialized for 2D grids, eliminating the need to manually connect points. It is ideal for factory floors, warehouse logistics, and NPC pathfinding in management sims.
class_name LogisticsGrid extends Nodevar _astar_grid: AStarGrid2Dfunc _ready() -> void:_astar_grid = AStarGrid2D.new()_astar_grid.region = Rect2i(0, 0, 100, 100)_astar_grid.cell_size = Vector2(32, 32)_astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES_astar_grid.update()## Mark a cell as impassable (e.g., placing a new machine)func place_obstacle(cell_coords: Vector2i) -> void:if _astar_grid.is_in_bounds(cell_coords.x, cell_coords.y):_astar_grid.set_point_solid(cell_coords, true)## Retrieves the logistics path for an NPC workerfunc get_npc_path(start_cell: Vector2i, target_cell: Vector2i) -> Array[Vector2i]:return _astar_grid.get_id_path(start_cell, target_cell)
3. CSV-to-Resource Workflow (Rapid Balancing)
Automate the conversion of spreadsheet data (CSV) into native .tres files. This allows game designers to balance thousands of entities in Excel/Google Sheets and "bake" them into performant Godot resources.
@toolclass_name CSVResourceBaker extends EditorScriptfunc _run() -> void:var csv_path := "res://data/balancing_sheet.csv"var output_dir := "res://data/generated_items/"var file := FileAccess.open(csv_path, FileAccess.READ)if not file: returnvar rows := file.get_as_text().split("\n", false)for i in range(1, rows.size()): # Skip headervar columns := rows[i].split(",", false)if columns.size() < 2: continuevar item_name: String = columns[0].strip_edges()var base_value: int = columns[1].to_int()var new_item := ItemResource.new()new_item.item_name = item_namenew_item.base_value = base_valueResourceSaver.save(new_item, output_dir + item_name.to_lower() + ".tres")
- Master Skill: godot-master