5 Commits

57 changed files with 1282 additions and 365 deletions

View File

@@ -5,23 +5,61 @@ Godot 4.6 idle-game; code is GDScript + `.tscn` scenes.
Use `GODOT_BIN` for your local editor binary (e.g. `godot`, `godot4`, or full path).
Run project: `"$GODOT_BIN" --path .`
Headless smoke/lint parse: `"$GODOT_BIN" --headless --path . --quit`
Script syntax check (single script): `"$GODOT_BIN" --headless --check-only --script res://big_number.gd`
Tests: no test framework is currently committed (`tests/` and `addons/gut` are absent).
Single-test command: N/A right now; if GUT is added, use `"$GODOT_BIN" --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/<file>.gd`.
Script syntax check (single script): `./scripts/check_syntax.sh <path>` (e.g. `./scripts/check_syntax.sh res://core/level_game_state.gd`)
Run single test: `./scripts/run_test.sh res://tests/<file>.gd`
Run all tests: `./scripts/run_all.sh`
Tests are custom headless scripts in `tests/` using `LevelGameState` simulation (no GUT).
## Architecture And Structure
`project.godot` defines autoload singletons: `GameState` (runtime currency state + save/load) and `CurrencyGeneratorDatabase` (loads `generator_data.json`).
`big_number.gd` (`class_name BigNumber`) is the internal numeric API for huge-value math, comparisons, formatting, and serialization.
`big_number_museum.tscn` + `big_number_museum.gd` is the current main playable scene/prototype UI.
`currency_generator.gd`, `currency_label.gd`, and `big_number_progress_bar.gd` are UI/gameplay adapters that react to `GameState` signals.
Persistence is local JSON only: `user://idle_save.json`; there is no external DB/server/backend.
### No Autoloads
There are **no autoload singletons**. All state lives in scene nodes. The project was fully decoupled from globals.
### Main Scene
`run/main_scene` points to `docs/gyms/tiny_sword/tiny_sword.tscn` (uid `dadeowsvaywpp`). This is the canonical playable scene. `docs/museums/big_number_museum.tscn` is a secondary prototype/testing scene.
### Core Systems (`core/`)
- **`core/big_number.gd`** — `class_name BigNumber`: numeric API for huge-value math, comparisons, formatting, and serialization (`{m: float, e: int}` scientific notation).
- **`core/level_game_state.gd`** — `class_name LevelGameState` (extends `Node`): the central runtime state node. Owns all currency balances, generator states, buff levels, goal completion, research XP, prestige buff unlocks, and save/load. Assigned catalogue resources via `@export`:
- `currency_catalogue``CurrencyCatalogue` (defines all currencies)
- `buff_catalogue``BuffCatalogue` (generator buffs)
- `goal_catalogue``GoalCatalogue` (achievement goals)
- `research_catalogue``ResearchCatalogue` (research tracks)
- `prestige_buff_catalogue``PrestigeBuffCatalogue` (permanent prestige upgrades)
- **`core/edge_scroll_camera.gd`** — camera with edge-scrolling and zoom.
### Subsystems (`core/<name>/`)
Each subsystem has its own directory with a `README.md`:
- **`core/currency/`** — `Currency` resource, `CurrencyCatalogue` resource, `currency_panel.gd` UI.
- **`core/generator/`** — `CurrencyGenerator` (production node), `CurrencyGeneratorData` (balancing resource), `GeneratorBuffData` (upgrades), `ResearchBuffCalculator` (static utility), `generator_container.gd` UI.
- **`core/goals/`** — `GoalData` (achievement resource), `GoalRequirementData` (individual criteria), `current_goal_panel.gd` UI.
- **`core/prestige/`** — `PrestigeManager` (child node of `LevelGameState` managing resets + multipliers), `PrestigeConfig`, `PrestigeBuffNode` (DAG graph node), `PrestigeBuffCatalogue`, UI panels (`prestige_panel`, `prestige_progress_bar`, `prestige_buff_graph_panel` with tiles).
- **`core/research/`** — `ResearchData` resource (XP thresholds, production multipliers per level), `ResearchCatalogue`, `research_panel.gd` + `research_row.gd` UI.
### Content Is Data-Driven (`docs/gyms/`)
All gameplay content (currencies, generators/buildings, buffs, goals, research, prestige config) is defined as `.tres` resource files under `docs/gyms/tiny_sword/` and composed in `tiny_sword.tscn`. No hardcoded JSON data files.
### Root-Level UI Helpers
- `currency_label.gd`, `currency_tile.tscn` — reusable currency display components.
- `big_number_progress_bar.gd` / `.tscn` — progress bar for BigNumber values.
- `generator_buff_tile.gd` / `.tscn` — individual buff upgrade tile.
- `goals_debug_ui.gd` / `.tscn` — debug overlay for goal state.
### Persistence
Local JSON only: `LevelGameState` saves to the path set in its `save_file_path` export (e.g. `user://tiny_sword_save.json`). No external DB/server/backend. Save format version is currently 8.
### Testing
Custom headless test scripts in `tests/` that instantiate `LevelGameState` directly (no scene tree needed). Helper scripts in `scripts/`:
- `check_syntax.sh` — single-file Godot syntax check.
- `run_test.sh` — run a single test script headless.
- `run_all.sh` — run all tests.
## Code Style And Conventions
Prefer typed GDScript (`var x: Type`, `func f() -> void`) and keep shared models/components as `class_name` scripts.
Naming: `snake_case` for vars/functions/signals, `PascalCase` for classes, `UPPER_SNAKE_CASE` for constants.
Follow existing Godot style: tabs/consistent indentation, early returns, and short functions grouped by purpose.
When reading files/JSON, always guard `FileAccess.open(...)` and validate parsed types before indexing.
Use signals (already in `GameState`) for UI updates instead of polling or cross-node direct mutation.
Use signals (emitted by `LevelGameState`) for UI updates instead of polling or cross-node direct mutation.
No Cursor/Claude/Windsurf/Cline/Goose/Copilot rule files were found in this repository, so this file is the canonical agent guidance.
Avoid trivial one-line wrapper/helper functions that only forward or repack data. Inline the logic at the real call site unless the wrapper adds meaningful abstraction or is reused enough to justify it.
Resource type safety: Use the specific resource. Avoid as much as possible explicit `Resource` usage.
Resource type safety: Use the specific resource type. Avoid the bare `Resource` type. Prefer `Currency`, `CurrencyGeneratorData`, `GeneratorBuffData`, `GoalData`, `ResearchData`, `PrestigeBuffNode`, etc.

283
TODO.md
View File

@@ -1,283 +0,0 @@
# TODO
## Ascension Buff Graph — Needs Review Before Implementation
### Goal
Add a permanent buff graph system:
- **Separate** from existing `GeneratorBuffData` (which resets on prestige).
- **Permanent** — prestige does NOT reset ascension buff state.
- **Unlocked** by spending ascension currency (earned during prestige).
- **Graph/DAG-structured** — nodes have prerequisites (parent_ids) that must be met before a node can be purchased.
- **One-shot unlocks** — each node is either locked or unlocked. No repeatable levels.
### Data Structure
#### `AscensionBuffNode` (Resource)
Single node in the graph.
| Field | Type | Notes |
|-------|------|-------|
| `id` | `StringName` | Unique node ID |
| `display_name` | `String` | UI label |
| `description` | `String` (multiline) | Tooltip |
| `icon` | `Texture2D` | Visual |
| `parent_ids` | `Array[StringName]` | Prerequisites (DAG — multiple parents allowed) |
| `effect_type` | `enum EffectType` | What this buff does |
| `effect_value` | `float` | Effect magnitude |
| `target_id` | `StringName` | Target generator/currency for type-specific effects (empty = global) |
| `cost_mantissa` | `float` | Ascension currency cost |
| `cost_exponent` | `int` | |
| `tier` | `int` | Row in graph UI (visual grouping) |
| `x_position` | `float` | Column in graph UI |
**Effect types** (extensible enum):
```
GLOBAL_PRODUCTION_MULTIPLIER → multiply all generator output
GENERATOR_PRODUCTION_MULTIPLIER → multiply one generator's output
CURRENCY_PER_RUN → start each prestige with this currency
COST_REDUCTION → reduce all generator purchase costs
PRESTIGE_GAIN_MULTIPLIER → earn more ascension currency per prestige
GENERATOR_COUNT_MULTIPLIER → scaling based on owned generator count
RESEARCH_XP_MULTIPLIER → faster research leveling
```
#### `AscensionBuffCatalogue` (Resource)
Flat list of nodes. The graph emerges from `parent_ids`. Follows existing catalogue pattern (`BuffCatalogue`, `GoalCatalogue`, etc.).
| Field | Type |
|-------|------|
| `nodes` | `Array[AscensionBuffNode]` |
#### State in `LevelGameState`
```gdscript
@export var ascension_buff_catalogue: AscensionBuffCatalogue
var _ascension_buff_unlocked: Dictionary = {} # {StringName: bool} — node_id → unlocked
```
- Never cleared by `reset_for_prestige()` (unlike `_buff_levels` for generator buffs, which IS cleared).
- Normalized/sanitized on load (like generator states).
### Save Format
- Bump `CURRENT_SAVE_FORMAT_VERSION` from 7 → 8.
- New key: `"ascension_buff_unlocked"` → array of unlocked node_id strings: `["strong_start", "efficiency"]`.
### API on LevelGameState
| Method | Purpose |
|--------|---------|
| `purchase_ascension_buff(buff_id) → bool` | Spend ascension currency, unlock node, emit signal, save |
| `can_purchase_ascension_buff(buff_id) → bool` | All parent_ids unlocked? Not already unlocked? Enough currency? |
| `is_ascension_buff_unlocked(buff_id) → bool` | Check if node is unlocked |
| `get_ascension_buff_cost(buff_id) → BigNumber` | Fixed cost for the node |
| `get_ascension_buff_multiplier(effect_type, target_id) → float` | Walk graph, compute combined effect from unlocked nodes |
| `get_available_ascension_buffs() → Array[AscensionBuffNode]` | Nodes whose prerequisites are all met |
### Signals
```gdscript
signal ascension_buff_unlocked(buff_id: StringName)
```
Ascension currency balance changes are covered by the existing `currency_changed` signal — no separate signal needed.
### Prestige Integration
No changes needed to `PrestigeManager``_reset_all_buff_levels()` only touches `_buff_levels` (generator buffs), not `_ascension_buff_unlocked`. Ascension currency earning already emits `currency_changed` via `add_currency_by_id()`. The new dictionary naturally survives the reset.
### Purchase Flow
```
Player clicks unlock node
→ can_purchase_ascension_buff(node_id)
→ All parent_ids unlocked?
→ Not already unlocked?
→ Enough ascension currency?
→ YES: spend_currency_by_id(ascension_currency, cost)
→ _ascension_buff_unlocked[node_id] = true
→ emit ascension_buff_unlocked
→ save_game()
```
### Example Graph Content (Illustrative)
```
Tier 0 (roots):
strong_start → +10 gold/run cost: 1 parents: []
efficiency → +10% global prod cost: 1 parents: []
thrifty → -5% gen costs cost: 2 parents: []
Tier 1:
better_start → +100 gold/run cost: 3 parents: [strong_start]
enhanced_efficiency → +25% global prod cost: 3 parents: [efficiency]
double_dip → +50% gold from forestry cost: 5 parents: [strong_start, efficiency]
Tier 2:
power_surge → +100% global prod cost: 10 parents: [better_start, enhanced_efficiency]
```
### Files to Create/Modify
| File | Action | Purpose |
|------|--------|---------|
| `core/ascension/ascension_buff_node.gd` | Create | Node resource |
| `core/ascension/ascension_buff_catalogue.gd` | Create | Catalogue resource |
| `core/ascension/ascension_buff_graph_panel.gd` | Create | Separate-screen UI panel |
| `core/level_game_state.gd` | Modify | Add catalogue ref, `_ascension_buff_unlocked`, purchase/query methods, save/load, signal |
| `docs/gyms/tiny_sword/ascension/*.tres` | Create | `.tres` resources for actual graph content |
---
## Implementation Plan
### Step 1: `core/ascension/ascension_buff_node.gd`
Create new `Resource` class.
- `class_name AscensionBuffNode extends Resource`
- `enum EffectType` with all seven types from the data structure table
- `@export` fields: `id`, `display_name`, `description` (multiline), `icon`, `parent_ids`, `effect_type`, `effect_value`, `target_id`, `cost_mantissa`, `cost_exponent`, `tier`, `x_position`
- Helper methods:
- `get_cost() → BigNumber` — returns `BigNumber.new(cost_mantissa, cost_exponent)`
- `has_parents() → bool` — returns `not parent_ids.is_empty()`
- `all_parents_unlocked(unlocked: Dictionary) → bool` — checks every id in `parent_ids` is in the dict as `true`
- `is_valid() → bool` — id not empty, cost_mantissa > 0
### Step 2: `core/ascension/ascension_buff_catalogue.gd`
Create new `Resource` class. Follows `BuffCatalogue` pattern exactly.
- `class_name AscensionBuffCatalogue extends Resource`
- `@export var nodes: Array[AscensionBuffNode] = []`
- `get_node_by_id(id: StringName) → AscensionBuffNode` — linear search
- `get_all_ids() → Array[StringName]` — collect non-empty ids
- `get_root_nodes() → Array[AscensionBuffNode]` — nodes with empty `parent_ids`
- `get_children_of(parent_id: StringName) → Array[AscensionBuffNode]` — nodes that list this id in `parent_ids`
### Step 3: Modify `core/level_game_state.gd`
**3a. Add export, state, signal, constants**
- Add `@export var ascension_buff_catalogue: AscensionBuffCatalogue` in the export section
- Add `var _ascension_buff_unlocked: Dictionary = {}` in state variables
- Add signal: `signal ascension_buff_unlocked(buff_id: StringName)` in signals section
- Add constant: `const ASCENSION_BUFF_UNLOCKED_KEY: String = "ascension_buff_unlocked"` in save/load constants
- Bump `CURRENT_SAVE_FORMAT_VERSION` from 7 to 8
**3b. Add initialization in `_ready()`**
After `_initialize_catalogues()`, call a new `_initialize_ascension_buffs()` method that:
- Iterates `ascension_buff_catalogue.nodes` (if catalogue is set)
- For each node id not already in `_ascension_buff_unlocked`, sets it to `false`
**3c. Add save serialization**
In `save_game()`, add:
```gdscript
ASCENSION_BUFF_UNLOCKED_KEY: _serialize_ascension_buff_unlocked(),
```
New private method `_serialize_ascension_buff_unlocked() → Array`:
- Returns an array of node id strings where `_ascension_buff_unlocked[id] == true`
**3d. Add load deserialization**
In `load_game()`, under the save format version check, add:
```gdscript
if parsed_data.has(ASCENSION_BUFF_UNLOCKED_KEY):
_deserialize_ascension_buff_unlocked(parsed_data.get(ASCENSION_BUFF_UNLOCKED_KEY))
```
New private method `_deserialize_ascension_buff_unlocked(raw: Variant) → void`:
- If raw is Array, for each string element set `_ascension_buff_unlocked[StringName(s)] = true`
- Then call `_initialize_ascension_buffs()` to fill in any missing node ids as `false`
**3e. Add API methods**
In a new `#region ascension_buff` section:
- `purchase_ascension_buff(buff_id: StringName) → bool`
- Guard: if not `can_purchase_ascension_buff(buff_id)`, return false
- Get node from catalogue, get cost via `BigNumber` from node's mantissa/exponent
- Call `spend_currency_by_id(ascension_currency_id, cost)` — use `get_ascension_currency_id()` helper
- Set `_ascension_buff_unlocked[buff_id] = true`
- Emit `ascension_buff_unlocked.emit(buff_id)`
- `save_game()`
- Return true
- `can_purchase_ascension_buff(buff_id: StringName) → bool`
- Node must exist in catalogue
- Not already unlocked (`_ascension_buff_unlocked.get(buff_id, false) == false`)
- All parent_ids unlocked (use node's helper or iterate)
- Enough ascension currency (check via `get_currency_amount_by_id(ascension_currency_id)`)
- `is_ascension_buff_unlocked(buff_id: StringName) → bool`
- `return _ascension_buff_unlocked.get(buff_id, false)`
- `get_ascension_buff_cost(buff_id: StringName) → BigNumber`
- Get node, return `BigNumber.new(node.cost_mantissa, node.cost_exponent)`
- `get_ascension_buff_multiplier(effect_type: int, target_id: StringName = &"") → float`
- Iterate all unlocked nodes matching the effect type
- Multiply their `effect_value` together
- If `target_id` is non-empty, filter nodes whose `target_id` matches or is empty (global) — TBD based on type semantics
- Return the product (starting from 1.0)
- `get_available_ascension_buffs() → Array[AscensionBuffNode]`
- Iterate all catalogue nodes not yet unlocked where all parent_ids are unlocked
- `get_ascension_currency_id() → StringName` (private helper or public)
- Look up the ascension currency ID from PrestigeManager's config, or use a well-known ID like `&"ascension"`. Prefer using the catalogue to determine it. For now, return `&"ascension"` and validate it exists in the currency catalogue.
**3f. Ensure prestige does NOT reset ascension buffs**
Verify `reset_for_prestige()` does not touch `_ascension_buff_unlocked`. It currently clears `_buff_levels` (generator buffs), `generator_states`, `research_xp`, `research_levels`, goals, etc. No change needed — `_ascension_buff_unlocked` is a separate dictionary that is not listed in the reset logic.
### Step 4: Create `core/ascension/ascension_buff_graph_panel.gd`
Create a new `PanelContainer`-based UI. This is a separate screen — it can be toggled visible/hidden (e.g., via a button in the main UI).
- `class_name AscensionBuffGraphPanel extends PanelContainer`
- `@onready` references to:
- Title label
- `ScrollContainer`/`VBoxContainer` for tier rows
- Ascension currency balance label
- `_game_state: LevelGameState` — found via `find_parent("LevelGameState")`
- In `_ready()`:
- Connect `game_state.ascension_buff_unlocked` → refresh
- Connect `game_state.currency_changed` → refresh ascension currency label
- Call `_build_graph()`
- `_build_graph()`:
- Clear existing rows
- Group all catalogue nodes by `tier`, sort tiers ascending
- For each tier, create an `HBoxContainer` row
- For each node in the tier (sorted by `x_position`), instantiate a tile scene
- **Tile scene** (`ascension_buff_graph_tile.tscn` + `.gd`):
- A small `Button`/`Panel` showing:
- Node icon
- Node display_name
- Effect description (e.g., "+10% global production")
- Cost in ascension currency
- Visual state: locked (greyed out), available (highlighted, purchasable), unlocked (green check)
- On press: call `_game_state.purchase_ascension_buff(node_id)`
- Connect to `ascension_buff_unlocked` and `currency_changed` to re-evaluate state
- Ascension currency label at the top showing current balance
### Step 5: Create content `.tres` files (deferred)
Content is not defined yet per resolved question 4. When ready:
- Create `docs/gyms/tiny_sword/ascension/` directory
- Create `ascension_buff_catalogue.tres` resource with nodes
- Wire it into the `LevelGameState` node in `tiny_sword.tscn` via editor
---
### Resolved Questions
1. **Effect stacking**: Multiply — consistent with the existing generator buff multiplier pattern.
2. **UI placement**: A separate screen.
3. **Ascension currency spending**: Spend through `LevelGameState.spend_currency_by_id()` — ascension currency is already in the currency catalogue.
4. **Content**: None at the moment — define later.

View File

@@ -101,6 +101,9 @@ func _ready() -> void:
_generator_id = _resolve_generator_id()
cycle_progress_seconds = 0.0
game_state.ready.connect(_on_game_state_ready)
func _on_game_state_ready() -> void:
if game_state:
game_state.currency_changed.connect(_on_currency_changed)
game_state.generator_state_changed.connect(_on_generated_state_changed)
@@ -716,11 +719,13 @@ func _default_owned_state() -> int:
## Area2D callback: pointer entered generator interaction region.
func _on_area_2d_mouse_entered() -> void:
_mouse_entered = true
if info_generator_container:
info_generator_container.visible = true
## Area2D callback: pointer exited generator interaction region.
func _on_area_2d_mouse_exited() -> void:
_mouse_entered = false
if info_generator_container:
info_generator_container.visible = false

View File

@@ -66,7 +66,6 @@ func _on_generator_updated(_a = null, _b = null, _c = null, _d = null) -> void:
func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) -> void:
if generator_id == _generator_id:
visible = true
_refresh_generator_ui()
func _on_buy_buff_pressed(buff_id: StringName) -> void:

View File

@@ -601,6 +601,7 @@ func reset_for_prestige(reset_total_currency: bool = false, emit_currency_signal
var research_ids: Array = research_xp.keys()
research_xp.clear()
research_levels.clear()
research_workers = 0
for research_id in research_ids:
research_xp_changed.emit(research_id, BigNumber.from_float(0.0))

View File

@@ -109,8 +109,8 @@ func _on_button_pressed() -> void:
_game_state.purchase_prestige_buff(prestige_buff.id)
func _on_prestige_buff_unlocked(buff_id: StringName) -> void:
if prestige_buff and buff_id == prestige_buff.id:
func _on_prestige_buff_unlocked(_buff_id: StringName) -> void:
# Any buff unlock may change this tile's parent-availability, so always refresh.
_refresh_state()

View File

@@ -185,6 +185,7 @@ func perform_prestige() -> bool:
game_state.reset_for_prestige(true, false, preserve_currencies)
_reset_all_buff_levels()
_reinitialize_generators_after_prestige_reset()
_reset_alchemy_workers_after_prestige()
if game_state:
game_state.emit_currency_changed_for_all()
_initialize_run_tracking_from_current_state()
@@ -469,6 +470,16 @@ func _reinitialize_generators_after_prestige_reset() -> void:
generator_node.call("reset_runtime_state_for_prestige")
func _reset_alchemy_workers_after_prestige() -> void:
var scene_root: Node = get_tree().current_scene
if scene_root == null:
return
var alchemy_panels: Array[Node] = scene_root.find_children("*", "AlchemyCurrenciesPanel", true, false)
for panel in alchemy_panels:
if panel != null and panel.has_method("reset_workers"):
panel.reset_workers()
func _deserialize_big_number(raw_value: Variant) -> BigNumber:
if raw_value is Dictionary:
return BigNumber.deserialize(raw_value)

View File

@@ -73,7 +73,6 @@ func _refresh_ui() -> void:
visible = false
return
visible = true
var total: BigNumber = manager.get_total_prestige()
var pending: BigNumber = manager.calculate_pending_gain()
var multiplier: float = float(manager.get_total_multiplier())

View File

@@ -0,0 +1,388 @@
# Tiny Sword — Progression Plan
Full progression design for a playable idle game, from first click to victory.
## Overview
The game has 8 phases. Each phase is gated behind a goal. Completing the goal unlocks new buildings, buffs, or systems that accelerate progress toward the next phase.
The Devil Idol is visible from the start as a long-term goal — destroying it is the one-time win condition. Prestige loops make it asymptotically easier.
---
## Phase Table
| Phase | Goal | Threshold | Unlocks |
|---|---|---|---|
| 1 | *(none)* | Start | Gold Mine (click only, no auto-production) |
| 2 | `gold_total_30` | 30 total gold | Gold Clicker, Gold Dynamo, Summon Worker buffs |
| 3 | `gold_total_1300` | 1,300 total gold | Farm building (food economy begins) |
| 4 | `gold_total_13k` | 13,000 total gold | Forestry building (wood economy), Farm Dynamo, Forest Dynamo |
| 5 | `gold_350k_wood_20k` | 350K gold + 20K wood | **Prestige** (Castle) + **Research** (Monastery) |
| 6 | `gold_2M_food_200k_wood_200k` | 2M gold + 200K food + 200K wood | **Alchemy Tower** |
| 7 | *(post-alchemy)* | — | Alchemy buffs (spend mana stone / cognite / philosopher stone) |
| 8 | `gold_50M_food_5M_wood_5M` | 50M gold + 5M food + 5M wood | **Barracks** + **Devil Idol fight** |
> **Note**: Phase 7 has no dedicated goal — it unlocks implicitly when the Alchemy Tower is active and the player can craft alchemy resources. The Phase 8 goal only needs to be reachable after Alchemy buffs have been applied.
---
## Phase Details
### Phase 1 — Start
**State**: Gold Mine is visible and unlocked, 0 owned, 0 workers.
**Action**: Player clicks the Gold Mine to earn gold manually (1 gold / click, 0.2s cooldown).
**Exit**: Accumulate 30 total gold → Phase 2.
---
### Phase 2 — First Automation
**Goal**: `gold_total_30` (30 total gold)
**Unlocks**:
| Buff | Kind | Cost | Effect | Max Level |
|---|---|---|---|---|
| Gold Clicker | MANUAL_CLICK_MULTIPLIER | Gold | ×(1 + 1×level) click multiplier | 4 |
| Gold Dynamo | AUTO_PRODUCTION_MULTIPLIER | Food | ×(1 + 1×level) auto prod | 10 |
| Summon Worker | RESOURCE_PURCHASE | Gold | Grants 1 worker | ∞ |
**Design note**: Gold Dynamo costs food but Farm isn't unlocked until Phase 3. This is intentional — the buff is visible but unusable until food flows. It creates anticipation.
**Exit**: Buy Summon Worker (200 gold) → get 1 worker → buy first Gold Mine (1 worker) → auto-production begins at 20 gold/s. Accumulate 1,300 total gold → Phase 3.
---
### Phase 3 — Food Economy
**Goal**: `gold_total_1300` (1,300 total gold)
**Unlocks**:
- **Farm** building — starts with 1 owned, produces 20 food/s, purchasable with workers
**New loop**: Gold → workers → more Gold Mines + more Farms. Food accumulates. Gold Dynamo (unlocked in Phase 2) now becomes purchasable with food, multiplying gold mine output.
**Exit**: Accumulate 13,000 total gold → Phase 4.
---
### Phase 4 — Wood Economy
**Goal**: `gold_total_13k` (13,000 total gold)
**Unlocks**:
- **Forestry** building — starts with 1 owned, produces 20 wood/s, purchasable with workers
- **Farm Dynamo** buff — AUTO_PRODUCTION_MULTIPLIER for Farm, costs food, max 10
- **Forest Dynamo** buff — AUTO_PRODUCTION_MULTIPLIER for Forestry, costs food, max 10
**New loop**: Wood flows alongside gold and food. Three parallel production lines scale independently.
**Exit**: Accumulate 350,000 gold + 20,000 wood → Phase 5.
---
### Phase 5 — Prestige + Research
**Goal**: `gold_350k_wood_20k` (350,000 total gold + 20,000 total wood)
**Unlocks**:
- **Prestige panel** at Castle becomes visible and interactive
- **Research panel** at Monastery becomes visible and interactive
#### Prestige System
| Config Property | Value |
|---|---|
| Source basis | RUN_TOTAL (gold earned this run) |
| Formula | POWER, exponent 0.5, scale 1.0 |
| Threshold | 50,000 gold (per prestige point) |
| Rounding | FLOOR |
**First prestige estimate**: At 350K gold → `floor(sqrt(350000/50000))` = `floor(sqrt(7))` = `floor(2.64)` = **2 ascension points**.
**Second run**: With prestige buffs applied and ~500K gold → `floor(sqrt(10))` = **3 ascension points**.
`total_prestige_earned` persists across resets — it's a strictly increasing counter that never resets. It feeds the prestige multiplier (`base_multiplier + total_prestige_earned × multiplier_per_prestige`).
**Prestige Buff Graph** (purchasable with ascension points):
| Node | Cost | Prerequisites | Effect |
|---|---|---|---|
| Golden Touch | 1 ascension | *(none)* | Gold Mine ×2 production |
| Fertile Grounds | 2 ascension | Golden Touch | Farm ×2 production |
| Wood Touch | 3 ascension | Fertile Grounds | Forestry ×2 production |
| Martial Prowess | 5 ascension | Wood Touch | Barracks ×2 unit production *(Phase 8)* |
#### Research System
Three research tracks at the Monastery:
| Track | Linked Generator | XP Source |
|---|---|---|
| Gold Mine Research | Gold Mine | Gold production |
| Farm Research | Farm | Food production |
| Forestry Research | Forestry | Wood production |
One track active at a time. XP scales with workers assigned to research (`research_workers`). Default: 0.01 XP per currency unit, 100 XP for level 1, ×1.5 growth per level, +0.1 multiplier per level.
**Exit**: Accumulate 2M gold + 200K food + 200K wood → Phase 6.
---
### Phase 6 — Alchemy Tower
**Goal**: `gold_2M_food_200k_wood_200k` (2,000,000 total gold + 200,000 food + 200,000 wood)
**Unlocks**:
- **Alchemy Tower** building — passively produces Magic Gold
#### Alchemy Tower Mechanics
- Cycles of base 5 seconds, producing 1 Magic Gold per cycle
- Cycle time grows ×1.2 per completed cycle
- Workers can be assigned (1 worker each, up to 10) for +10% speed each
- Workers assigned here are separate from research workers (spent from worker currency)
#### Crafting Recipes
| Recipe | Input | Output |
|---|---|---|
| Mana Stone | 10 Magic Gold | 1 Mana Stone |
| Cognite | 25 Magic Gold | 1 Cognite |
| Philosopher Stone | 10 Mana Stone + 10 Cognite | 1 Philosopher Stone |
**Exit**: Build up alchemy resources → Phase 7 (implicit).
---
### Phase 7 — Alchemy Buffs
**Prerequisite**: Alchemy Tower active, player can craft resources.
Three new buffs, purchasable with alchemy resources:
| Buff | Cost Currency | Target | Effect | Max Level |
|---|---|---|---|---|
| Fertile Infusion | Mana Stone | Farm | ×2 production per level | 5 |
| Sylvan Cognite | Cognite | Forestry | ×2 production per level | 5 |
| Opus Magnum | Philosopher Stone | Gold Mine | ×3 production per level | 5 |
Added to the buff catalogue. Available via the existing buff purchase UI in each generator's panel.
**Exit**: Accumulate 50M gold + 5M food + 5M wood → Phase 8.
---
### Phase 8 — Devil Idol (Finale)
**Goal**: `gold_50M_food_5M_wood_5M` (50,000,000 total gold + 5,000,000 food + 5,000,000 wood)
**Unlocks**:
- **Barracks** building — produces Military Units (pure damage currency)
- **Devil Idol** becomes targetable
#### Barracks
| Property | Value |
|---|---|
| Produces | `unit` currency |
| Purchase cost | Workers |
| Unlock goal | Phase 8 goal |
| Research track | Shares with ? or none (TBD) |
| Prestige buff | Martial Prowess (×2 unit production) |
#### Devil Idol
| Property | Value |
|---|---|
| Visible from | Start (always visible in world) |
| Targetable from | Phase 8 |
| HP | `1e12` (1,000,000,000,000) — defined in an exported resource |
| Damage mechanic | Click idol to spend 1 unit → deal 1 damage. Or "Send Army" button for bulk spend. |
| Win condition | HP ≤ 0 → victory signal (TBD: victory screen, credits, etc.) |
#### Asymptotic Loop
Without prestige: defeating the idol would require 10^12 units, which with baseline production is essentially infinite.
With prestige: each loop applies Martial Prowess (+×2 per level) to Barracks production. A player with 5 ascension points in Martial Prowess produces ×32 units per cycle. As they earn more ascension points across runs, unit production scales multiplicatively, asymptotically reducing the "time to kill."
The idol HP never resets — it's persistent across prestige. Each prestige loop chips away at it.
---
## Currency Catalogue (Complete)
| Currency | ID | Produced By | Used For |
|---|---|---|---|
| Gold | `gold` | Gold Mine | Buff purchases, worker generation |
| Worker | `worker` | Summon Worker buff | Purchasing generators, research assignment, alchemy workers |
| Food | `food` | Farm | Buff purchases |
| Wood | `wood` | Forestry | Goal requirements |
| Ascension | `ascension` | Prestige resets | Prestige buff graph |
| Magic Gold | `magic_gold` | Alchemy Tower | Crafting mana stones + cognite |
| Mana Stone | `mana_stone` | Craft (10 magic gold) | Crafting philosopher stones, Fertile Infusion buff |
| Cognite | `cognite` | Craft (25 magic gold) | Crafting philosopher stones, Sylvan Cognite buff |
| Philosopher Stone | `philosoper_stone` | Craft (10 mana + 10 cognite) | Opus Magnum buff |
| Unit | `unit` | Barracks | Devil Idol damage |
---
## Buff Catalogue (Complete)
| Buff | ID | Kind | Cost | Unlock Goal | Max Lvl | Target |
|---|---|---|---|---|---|---|
| Gold Clicker | `gold_click_focus` | CLICK_MULT | Gold | `gold_total_30` | 4 | Gold Mine |
| Gold Dynamo | `gold_auto_flux` | AUTO_MULT | Food | `gold_total_30` | 10 | Gold Mine |
| Summon Worker | `summon_worker` | RESOURCE | Gold | `gold_total_30` | ∞ | Worker |
| Farm Dynamo | `farm_auto_flux` | AUTO_MULT | Food | `gold_total_13k` | 10 | Farm |
| Forest Dynamo | `forestry_auto_flux` | AUTO_MULT | Food | `gold_total_13k` | 10 | Forestry |
| Fertile Infusion | *(new)* | AUTO_MULT | Mana Stone | *(alchemy active)* | 5 | Farm |
| Sylvan Cognite | *(new)* | AUTO_MULT | Cognite | *(alchemy active)* | 5 | Forestry |
| Opus Magnum | *(new)* | AUTO_MULT | Philo Stone | *(alchemy active)* | 5 | Gold Mine |
---
## Goal Catalogue (Complete)
| Goal ID | Requirements | Unlock Behavior | Phase |
|---|---|---|---|
| `gold_total_30` | 30 gold | AUTOMATIC | 2 |
| `gold_total_1300` | 1,300 gold | MANUAL | 3 |
| `gold_total_13k` | 13,000 gold | MANUAL | 4 |
| `gold_350k_wood_20k` | 350,000 gold + 20,000 wood | MANUAL | 5 |
| `gold_2M_food_200k_wood_200k` *(new)* | 2M gold + 200K food + 200K wood | MANUAL | 6 |
| `gold_50M_food_5M_wood_5M` *(new)* | 50M gold + 5M food + 5M wood | MANUAL | 8 |
---
## Prestige Buff Graph (Complete)
```
Golden Touch (1pt)
Fertile Grounds (2pt)
Wood Touch (3pt)
Martial Prowess (5pt)
```
| Node | Cost | Prerequisites | Effect | Target |
|---|---|---|---|---|
| Golden Touch | 1 | none | ×2 production | Gold Mine |
| Fertile Grounds | 2 | Golden Touch | ×2 production | Farm |
| Wood Touch | 3 | Fertile Grounds | ×2 production | Forestry |
| Martial Prowess | 5 | Wood Touch | ×2 production | Barracks |
---
## Buildings (Complete)
| Building | Scene | Generates | Cost Currency | Unlock Goal |
|---|---|---|---|---|
| Gold Mine | `gold_mine.tscn` | Gold | Worker | *(none)* |
| Farm | `farm.tscn` | Food | Worker | `gold_total_1300` |
| Forestry | `forestry.tscn` | Wood | Worker | `gold_total_13k` |
| Monastery | `monastery.tscn` | *(none — Research UI)* | — | `gold_350k_wood_20k` (panel visible) |
| Castle | `castle.tscn` | *(none — Prestige UI)* | — | `gold_350k_wood_20k` (panel visible) |
| Alchemy Tower | `alchemy_tower.tscn` | Magic Gold | — | `gold_2M_food_200k_wood_200k` *(new)* |
| Barracks | `barracks.tscn` *(new)* | Unit | Worker | `gold_50M_food_5M_wood_5M` *(new)* |
| Devil Idol | `devil_idol.tscn` *(new)* | *(none — damage sink)* | — | `gold_50M_food_5M_wood_5M` (targetable) |
---
## Implementation Checklist
### Phase 12 (already exists)
- [x] Gold Mine building + generator
- [x] Gold Clicker, Gold Dynamo, Summon Worker buffs
- [x] `gold_total_30` goal
### Phase 34 (already exists)
- [x] Farm building + generator
- [x] `gold_total_1300` goal
- [x] Forestry building + generator
- [x] Farm Dynamo, Forest Dynamo buffs
- [x] `gold_total_13k` goal
### Phase 5 — Prestige + Research Visibility
- [ ] **`castle.gd`**: Uncomment `_prestige_panel.visible = true`, gate behind `gold_350k_wood_20k` goal completion
- [ ] **`primary_prestige.tres`**: Tune basis → RUN_TOTAL, source → `"gold"`, threshold → 50,000, exponent → 0.5
- [ ] **`monastery.gd`**: Gate `_research_panel.visible` behind `gold_350k_wood_20k` goal completion
- [ ] **`prestige_buff_catalogue.tres`**: Add Wood Touch + Martial Prowess nodes
- [x] `gold_350k_wood_20k` goal (already exists)
### Phase 6 — Alchemy Tower Gating
- [ ] **New goal `.tres`**: `gold_2M_food_200k_wood_200k`
- [ ] **`ts_goal_catalogue.tres`**: Register new goal
- [ ] **`alchemy_tower_data.tres`** or **`alchemy_tower.tscn`**: Add `unlock_goal` field
- [ ] Wire unlock to Alchemy Tower visibility
### Phase 7 — Alchemy Buffs
- [ ] **New buff `.tres`**: Fertile Infusion (mana stone → farm, ×2, max 5)
- [ ] **New buff `.tres`**: Sylvan Cognite (cognite → forestry, ×2, max 5)
- [ ] **New buff `.tres`**: Opus Magnum (philosopher stone → gold mine, ×3, max 5)
- [ ] **`ts_buff_catalogue.tres`**: Register the 3 new buffs
### Phase 8 — Barracks + Devil Idol
- [ ] **New goal `.tres`**: `gold_50M_food_5M_wood_5M`
- [ ] **`ts_goal_catalogue.tres`**: Register new goal
- [ ] **New currency `.tres`**: Unit (`unit`)
- [ ] **`ts_currency_catalogue.tres`**: Register unit currency
- [ ] **New generator data `.tres`**: `barracks_generator.tres`
- [ ] **New scene**: `barracks.tscn` (NG2D, sprite, Area2D, CurrencyGenerator, GeneratorContainer)
- [ ] **New scene**: `devil_idol.tscn` (NG2D, sprite, Area2D, click handler, HP display, win logic)
- [ ] **New prestige buff `.tres`**: Martial Prowess (×2 barracks production, cost 5 ascension, parent: Wood Touch)
- [ ] **`prestige_buff_catalogue.tres`**: Register Martial Prowess
- [ ] **`tiny_sword.tscn`**: Place Barracks + Devil Idol in world
- [ ] Victory condition handling (TBD: signal, screen, save state)
### Research (Phase 5+, already functional)
- [x] ResearchData for Gold Mine, Farm, Forestry
- [x] ResearchPanel at Monastery
- [x] ResearchRow UI
- [x] XP generation in CurrencyGenerator
- [ ] *(optional)*: Add Barracks research track
- [ ] *(optional)*: Tune per-track values (XP required, multiplier growth)
---
## File Map
```
docs/gyms/tiny_sword/
├── PROGRESSION.md ← this file
├── tiny_sword.tscn ← main scene (add Barracks + Devil Idol)
├── currencies/
│ ├── ts_currency_catalogue.tres ← register "unit" currency
│ └── unit.tres ← NEW: unit currency definition
├── buffs/
│ ├── ts_buff_catalogue.tres ← register alchemy buffs
│ ├── fertile_infusion_buff.tres ← NEW
│ ├── sylvan_cognite_buff.tres ← NEW
│ └── opus_magnum_buff.tres ← NEW
├── goals/
│ ├── ts_goal_catalogue.tres ← register new phase 6 + 8 goals
│ ├── gold_2M_food_200k_wood_200k.tres ← NEW
│ └── gold_50M_food_5M_wood_5M.tres ← NEW
├── prestige/
│ ├── primary_prestige.tres ← tune: RUN_TOTAL, gold, 50K threshold
│ ├── prestige_buff_catalogue.tres ← register Wood Touch + Martial Prowess
│ ├── prestige_buff_wood_boost.tres ← existing (may need cost/parent update)
│ └── prestige_buff_barracks_boost.tres ← NEW
├── buildings/
│ ├── barracks/ ← NEW
│ │ ├── barracks.tscn
│ │ └── barracks_generator.tres
│ ├── devil_idol/ ← NEW
│ │ └── devil_idol.tscn
│ └── ... (existing buildings)
└── research/
└── ts_research_catalogue.tres ← (optional) add barracks research
```

View File

@@ -0,0 +1,17 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_buff"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="2_cost"]
[ext_resource type="Resource" uid="uid://bn4gm4sss85d0" path="res://docs/gyms/tiny_sword/goals/gold_50M_food_5M_wood_5M_goal.tres" id="3_goal"]
[resource]
script = ExtResource("1_buff")
id = &"barracks_food_boost"
target_ids = Array[StringName]([&"barracks"])
text = "Supply Lines"
unlock_goal = ExtResource("3_goal")
max_level = 50
effect_increment = 0.5
cost_currency = ExtResource("2_cost")
base_cost_mantissa = 50000.0
cost_multiplier = 1.15

View File

@@ -0,0 +1,17 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_buff"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_cost"]
[ext_resource type="Resource" uid="uid://bn4gm4sss85d0" path="res://docs/gyms/tiny_sword/goals/gold_50M_food_5M_wood_5M_goal.tres" id="3_goal"]
[resource]
script = ExtResource("1_buff")
id = &"barracks_gold_boost"
target_ids = Array[StringName]([&"barracks"])
text = "War Chest"
unlock_goal = ExtResource("3_goal")
max_level = 50
effect_increment = 0.5
cost_currency = ExtResource("2_cost")
base_cost_mantissa = 100000.0
cost_multiplier = 1.15

View File

@@ -0,0 +1,17 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_buff"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="2_cost"]
[ext_resource type="Resource" uid="uid://bn4gm4sss85d0" path="res://docs/gyms/tiny_sword/goals/gold_50M_food_5M_wood_5M_goal.tres" id="3_goal"]
[resource]
script = ExtResource("1_buff")
id = &"barracks_wood_boost"
target_ids = Array[StringName]([&"barracks"])
text = "Siege Works"
unlock_goal = ExtResource("3_goal")
max_level = 50
effect_increment = 0.5
cost_currency = ExtResource("2_cost")
base_cost_mantissa = 50000.0
cost_multiplier = 1.15

View File

@@ -0,0 +1,18 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://c0ed8k5nlm22p"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_buff"]
[ext_resource type="Resource" uid="uid://brctmnpmhjas6" path="res://docs/gyms/tiny_sword/currencies/mana_stone.tres" id="2_cost"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="3_target"]
[ext_resource type="Resource" uid="uid://bex2hsenbr54e" path="res://docs/gyms/tiny_sword/goals/gold_2M_food_200k_wood_200k_goal.tres" id="4_goal"]
[resource]
script = ExtResource("1_buff")
id = &"fertile_infusion"
target_ids = Array[StringName]([&"farm"])
text = "Fertile Infusion"
max_level = 5
unlock_goal = ExtResource("4_goal")
effect_increment = 1.0
cost_currency = ExtResource("2_cost")
base_cost_mantissa = 1.0
resource_target_currency = ExtResource("3_target")

View File

@@ -0,0 +1,19 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://xdr3fma7h2b6"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_buff"]
[ext_resource type="Resource" uid="uid://co4fiqgluwit0" path="res://docs/gyms/tiny_sword/currencies/philosoper_stone.tres" id="2_cost"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="3_target"]
[ext_resource type="Resource" uid="uid://bex2hsenbr54e" path="res://docs/gyms/tiny_sword/goals/gold_2M_food_200k_wood_200k_goal.tres" id="4_goal"]
[resource]
script = ExtResource("1_buff")
id = &"opus_magnum"
target_ids = Array[StringName]([&"goldmine"])
text = "Opus Magnum"
max_level = 5
unlock_goal = ExtResource("4_goal")
effect_increment = 2.0
cost_currency = ExtResource("2_cost")
base_cost_mantissa = 1.0
cost_multiplier = 2.0
resource_target_currency = ExtResource("3_target")

View File

@@ -0,0 +1,18 @@
[gd_resource type="Resource" script_class="GeneratorBuffData" format=3 uid="uid://bkqosgm72sb8w"]
[ext_resource type="Script" uid="uid://dnhocfsuvmeyb" path="res://core/generator/generator_buff_data.gd" id="1_buff"]
[ext_resource type="Resource" uid="uid://t6du7gm2ywbi" path="res://docs/gyms/tiny_sword/currencies/cognite.tres" id="2_cost"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="3_target"]
[ext_resource type="Resource" uid="uid://bex2hsenbr54e" path="res://docs/gyms/tiny_sword/goals/gold_2M_food_200k_wood_200k_goal.tres" id="4_goal"]
[resource]
script = ExtResource("1_buff")
id = &"sylvan_cognite"
target_ids = Array[StringName]([&"forestry"])
text = "Sylvan Cognite"
max_level = 5
unlock_goal = ExtResource("4_goal")
effect_increment = 1.0
cost_currency = ExtResource("2_cost")
base_cost_mantissa = 1.0
resource_target_currency = ExtResource("3_target")

View File

@@ -7,8 +7,14 @@
[ext_resource type="Resource" uid="uid://bjc6qmvr7pe12" path="res://docs/gyms/tiny_sword/buffs/spawn_worker_buff.tres" id="4_hnq3m"]
[ext_resource type="Resource" uid="uid://cg7os1rfknw05" path="res://docs/gyms/tiny_sword/buffs/farm_auto_flux_buff.tres" id="5_mii30"]
[ext_resource type="Resource" uid="uid://kamgujbqqhg3" path="res://docs/gyms/tiny_sword/buffs/forestry_auto_flux_buff.tres" id="6_k5yi4"]
[ext_resource type="Resource" uid="uid://c0ed8k5nlm22p" path="res://docs/gyms/tiny_sword/buffs/fertile_infusion_buff.tres" id="7_fertile"]
[ext_resource type="Resource" uid="uid://bkqosgm72sb8w" path="res://docs/gyms/tiny_sword/buffs/sylvan_cognite_buff.tres" id="8_sylvan"]
[ext_resource type="Resource" uid="uid://xdr3fma7h2b6" path="res://docs/gyms/tiny_sword/buffs/opus_magnum_buff.tres" id="9_opus"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/buffs/barracks_gold_boost_buff.tres" id="10_wc"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/buffs/barracks_food_boost_buff.tres" id="11_sl"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/buffs/barracks_wood_boost_buff.tres" id="12_sw"]
[resource]
script = ExtResource("2_iuxc1")
buffs = Array[ExtResource("1_up80l")]([ExtResource("2_dtvfl"), ExtResource("3_0bb41"), ExtResource("4_hnq3m"), ExtResource("5_mii30"), ExtResource("6_k5yi4")])
buffs = Array[ExtResource("1_up80l")]([ExtResource("2_dtvfl"), ExtResource("3_0bb41"), ExtResource("4_hnq3m"), ExtResource("5_mii30"), ExtResource("6_k5yi4"), ExtResource("7_fertile"), ExtResource("8_sylvan"), ExtResource("9_opus"), ExtResource("10_wc"), ExtResource("11_sl"), ExtResource("12_sw")])
metadata/_custom_type_script = "uid://ctc5yjlnvi0ok"

View File

@@ -98,7 +98,7 @@ func _update_worker_display() -> void:
var worker_currency: Currency = _get_worker_currency()
var can_assign: bool = false
if worker_currency != null and _alchemy_worker_count < 10:
if worker_currency != null and _alchemy_worker_count < _alchemy_tower.data.max_workers:
var worker_balance: BigNumber = _game_state.get_currency_amount(worker_currency)
can_assign = worker_balance.mantissa >= 1.0
@@ -159,3 +159,9 @@ func _on_assign_button_pressed() -> void:
func _on_craft_button_pressed(recipe: Variant) -> void:
craft_requested.emit(recipe)
## Called by PrestigeManager after prestige to reset assigned alchemy workers.
func reset_workers() -> void:
_alchemy_worker_count = 0
_update_worker_display()
alchemy_workers_changed.emit(0)

View File

@@ -1,6 +1,8 @@
class_name AlchemyTower
extends Node2D
const UNLOCK_GOAL_ID: StringName = &"gold_2M_food_200k_wood_200k"
## Emitted when production progress changes (0.0 to 1.0)
signal production_progress_updated(progress: float)
## Emitted when magic gold balance changes
@@ -16,6 +18,8 @@ signal production_completed(amount: BigNumber)
## Reference to game state
@onready var game_state: LevelGameState = find_parent("LevelGameState")
@onready var _alchemy_currencies_panel: AlchemyCurrenciesPanel = $AlchemyCurrenciesPanel
@onready var _sprite: Sprite2D = $Sprite2D
@onready var _area: Area2D = $Area2D
## Current production state
var production_time_elapsed: float = 0.0
@@ -36,17 +40,23 @@ func _ready() -> void:
# Initialize production time
current_production_time = data.base_production_time_seconds
# Register this tower as an available generator
game_state.set_generator_available(get_generator_id(), true)
# Hide tower until unlock goal is completed
_sprite.visible = false
_area.monitoring = false
# Connect to magic gold balance changes
game_state.currency_changed.connect(_on_currency_changed)
game_state.goal_completed.connect(_on_goal_completed)
# Connect panel signals if available
if alchemy_panel:
alchemy_panel.craft_requested.connect(_on_craft_requested)
alchemy_panel.alchemy_workers_changed.connect(_on_alchemy_workers_changed)
# If goal was already completed in a previous session, unlock immediately
if game_state.is_goal_completed(UNLOCK_GOAL_ID):
_unlock_tower()
# Initial progress update
production_progress_updated.emit(0.0)
@@ -200,3 +210,15 @@ func _on_area_2d_mouse_entered() -> void:
func _on_area_2d_mouse_exited() -> void:
_alchemy_currencies_panel.visible = false
func _on_goal_completed(goal_id: StringName) -> void:
if goal_id == UNLOCK_GOAL_ID:
_unlock_tower()
func _unlock_tower() -> void:
_sprite.visible = true
_area.monitoring = true
game_state.set_generator_available(get_generator_id(), true)
game_state.register_generator(get_generator_id(), true, true, 1)

View File

@@ -25,11 +25,12 @@ position = Vector2(0.5, 11)
shape = SubResource("RectangleShape2D_8pntr")
[node name="AlchemyCurrenciesPanel" parent="." unique_id=731368154 instance=ExtResource("2_8pntr")]
z_index = 1
visible = false
offset_left = 65.0
offset_top = -75.0
offset_right = 65.0
offset_bottom = -75.0
offset_left = 44.0
offset_top = -81.0
offset_right = 444.0
offset_bottom = 219.0
currency = ExtResource("7_b8p5n")
craft_recipes = ExtResource("5_recipes")

View File

@@ -4,3 +4,4 @@
[resource]
script = ExtResource("1_data")
max_workers = 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cx11iodvbcfb1"
path="res://.godot/imported/Barracks.png-6c10bd8efd919b8eb03e40d568e18cf1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/gyms/tiny_sword/buildings/barracks/Barracks.png"
dest_files=["res://.godot/imported/Barracks.png-6c10bd8efd919b8eb03e40d568e18cf1.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,25 @@
extends Node2D
const UNLOCK_GOAL_ID: StringName = &"gold_50M_food_5M_wood_5M"
@onready var _sprite: Sprite2D = $Sprite2D
@onready var _area: Area2D = $Area2D
@onready var _game_state: LevelGameState = find_parent("LevelGameState")
func _ready() -> void:
_sprite.visible = false
_area.monitoring = false
_game_state.goal_completed.connect(_on_goal_completed)
if _game_state.is_goal_completed(UNLOCK_GOAL_ID):
_unlock()
func _on_goal_completed(goal_id: StringName) -> void:
if goal_id == UNLOCK_GOAL_ID:
_unlock()
func _unlock() -> void:
_sprite.visible = true
_area.monitoring = true

View File

@@ -0,0 +1 @@
uid://xk7d3maod6uc

View File

@@ -0,0 +1,41 @@
[gd_scene format=3 uid="uid://cyvijgwj6l8vg"]
[ext_resource type="PackedScene" uid="uid://jeoiinukrrsp" path="res://core/generator/currency_generator.tscn" id="1_gen"]
[ext_resource type="Script" uid="uid://xk7d3maod6uc" path="res://docs/gyms/tiny_sword/buildings/barracks/barracks.gd" id="1_script"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/currencies/unit.tres" id="2_unit"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/buildings/barracks/barracks_generator.tres" id="3_data"]
[ext_resource type="Texture2D" uid="uid://cx11iodvbcfb1" path="res://docs/gyms/tiny_sword/buildings/barracks/Barracks.png" id="4_ud3ae"]
[ext_resource type="PackedScene" uid="uid://ckos7f22pnmyh" path="res://core/generator/generator_container.tscn" id="5_panel"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_barracks"]
size = Vector2(160, 175)
[node name="Barracks" type="Node2D" unique_id=1061432510]
script = ExtResource("1_script")
[node name="CurrencyGenerator" parent="." unique_id=427948318 node_paths=PackedStringArray("info_generator_container") instance=ExtResource("1_gen")]
currency = ExtResource("2_unit")
data = ExtResource("3_data")
press_buys_generator = false
info_generator_container = NodePath("../GeneratorContainer")
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1503773416]
texture = ExtResource("4_ud3ae")
[node name="Area2D" type="Area2D" parent="." unique_id=1808028142]
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=614030143]
position = Vector2(1, 19.5)
shape = SubResource("RectangleShape2D_barracks")
[node name="GeneratorContainer" parent="." unique_id=1905754561 node_paths=PackedStringArray("_generator") instance=ExtResource("5_panel")]
z_index = 1
visible = false
offset_left = 70.0
offset_top = -100.0
offset_right = 334.0
offset_bottom = 110.0
_generator = NodePath("../CurrencyGenerator")
[connection signal="mouse_entered" from="Area2D" to="CurrencyGenerator" method="_on_area_2d_mouse_entered"]
[connection signal="mouse_exited" from="Area2D" to="CurrencyGenerator" method="_on_area_2d_mouse_exited"]

View File

@@ -0,0 +1,18 @@
[gd_resource type="Resource" script_class="CurrencyGeneratorData" format=3]
[ext_resource type="Script" path="res://core/generator/currency_generator_data.gd" id="1_data"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/currencies/worker.tres" id="2_worker"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/goals/gold_50M_food_5M_wood_5M_goal.tres" id="3_goal"]
[resource]
script = ExtResource("1_data")
id = &"barracks"
name = "Barracks"
starts_unlocked = false
starts_available = false
initial_owned = 1
purchase_currency = ExtResource("2_worker")
unlock_goal = ExtResource("3_goal")
initial_cost = 1.0
coefficient = 1.0
initial_productivity = 1.0

View File

@@ -1,20 +1,27 @@
extends Node2D
const PRESTIGE_GOAL_ID: StringName = &"gold_350k_wood_20k"
@onready var _prestige_panel: PrestigePanel = $PrestigePanel
@onready var game_state: LevelGameState = find_parent("LevelGameState")
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.
_prestige_panel.visible = false
if game_state:
game_state.goal_completed.connect(_on_goal_completed)
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
pass
func _on_area_2d_mouse_entered() -> void:
## TODO: remove this comment to enable prestige panel
#_prestige_panel.visible = true
pass
if game_state and game_state.is_goal_completed(PRESTIGE_GOAL_ID):
_prestige_panel.visible = true
func _on_area_2d_mouse_exited() -> void:
_prestige_panel.visible = false
func _on_goal_completed(goal_id: StringName) -> void:
if goal_id == PRESTIGE_GOAL_ID:
# Prestige is now unlocked — panel will show on next hover.
pass

View File

@@ -23,16 +23,17 @@ position = Vector2(1, 22)
shape = SubResource("RectangleShape2D_tgvch")
[node name="PrestigePanel" parent="." unique_id=245519778 instance=ExtResource("3_l7gct")]
offset_left = 161.0
offset_top = -232.0
offset_right = 581.0
offset_bottom = -3.0
z_index = 1
offset_left = 142.0
offset_right = 562.0
offset_bottom = 229.0
[node name="PanelContainer" type="PanelContainer" parent="." unique_id=549346899]
offset_left = 160.0
offset_top = 5.0
offset_right = 575.0
offset_bottom = 201.0
z_index = 1
offset_left = 142.0
offset_top = -201.0
offset_right = 557.0
offset_bottom = -5.0
size_flags_horizontal = 3
size_flags_vertical = 3

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://rpedoq4102to"
path="res://.godot/imported/Castle.png-abe84e074a7668619903091d591ffce7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://docs/gyms/tiny_sword/buildings/devil_idol/Castle.png"
dest_files=["res://.godot/imported/Castle.png-abe84e074a7668619903091d591ffce7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View File

@@ -0,0 +1,142 @@
class_name DevilIdol
extends Node2D
const UNLOCK_GOAL_ID: StringName = &"gold_50M_food_5M_wood_5M"
const SAVE_KEY: String = "devil_idol_state"
const HP_KEY: String = "current_hp"
## Emitted when the idol takes damage.
signal idol_damaged(current_hp: BigNumber, max_hp: BigNumber)
## Emitted when the idol is destroyed (one-time win condition).
signal idol_destroyed()
## Maximum HP mantissa.
@export var max_hp_mantissa: float = 1.0
## Maximum HP exponent (default 1e12 = 1 trillion).
@export var max_hp_exponent: int = 12
@onready var game_state: LevelGameState = find_parent("LevelGameState")
@onready var _sprite: Sprite2D = $Sprite2D
@onready var _area: Area2D = $Area2D
@onready var _panel: PanelContainer = $IdolPanel
@onready var _hp_value: Label = $IdolPanel/MarginContainer/VBoxContainer/HPValue
@onready var _damage_button: Button = $IdolPanel/MarginContainer/VBoxContainer/DamageButton
@onready var _victory_label: Label = $IdolPanel/MarginContainer/VBoxContainer/VictoryLabel
var _max_hp: BigNumber
var _current_hp: BigNumber
var _is_targetable: bool = false
var _is_destroyed: bool = false
func _ready() -> void:
assert(game_state != null, "DevilIdol: LevelGameState parent not found")
_max_hp = BigNumber.new(max_hp_mantissa, max_hp_exponent)
_current_hp = BigNumber.new(max_hp_mantissa, max_hp_exponent)
# Load persisted HP from save data
_load_state()
# Panel hidden by default; shown on hover if unlocked
_panel.visible = false
_victory_label.visible = false
_damage_button.pressed.connect(_on_damage_button_pressed)
# Connect to goal completion for unlocking
game_state.goal_completed.connect(_on_goal_completed)
# Refresh button state when unit balance changes
game_state.currency_changed.connect(_on_currency_changed)
# If goal was already completed in a previous session, unlock immediately
if game_state.is_goal_completed(UNLOCK_GOAL_ID):
_unlock_idol()
_update_ui()
func _on_area_2d_mouse_entered() -> void:
if _is_targetable and not _is_destroyed:
_panel.visible = true
func _on_area_2d_mouse_exited() -> void:
_panel.visible = false
func _on_goal_completed(goal_id: StringName) -> void:
if goal_id == UNLOCK_GOAL_ID:
_unlock_idol()
func _on_currency_changed(currency_id: StringName, _new_amount: BigNumber) -> void:
if currency_id == &"unit":
_update_ui()
func _unlock_idol() -> void:
_is_targetable = true
func _on_damage_button_pressed() -> void:
if _is_destroyed:
return
var unit_balance: BigNumber = game_state.get_currency_amount_by_id(&"unit")
if unit_balance.mantissa <= 0.0:
return
# Spend all available units as damage
var damage: BigNumber = BigNumber.new(unit_balance.mantissa, unit_balance.exponent)
if game_state.spend_currency_by_id(&"unit", damage):
_current_hp = _current_hp.subtract(damage)
if _current_hp.mantissa <= 0.0:
_current_hp = BigNumber.new(0.0, 0)
_is_destroyed = true
idol_destroyed.emit()
_victory_label.visible = true
_panel.visible = true
idol_damaged.emit(_current_hp, _max_hp)
_save_state()
_update_ui()
func _update_ui() -> void:
if _is_destroyed:
_hp_value.text = "0 / %s — DESTROYED" % _max_hp.to_string_suffix(2)
_damage_button.disabled = true
_damage_button.text = "VICTORY!"
return
var progress_pct: float = 0.0
var max_float: float = _max_hp.to_float()
if max_float > 0.0:
var current_float: float = _current_hp.to_float()
progress_pct = clampf((1.0 - current_float / max_float) * 100.0, 0.0, 100.0)
_hp_value.text = "%s / %s (%.1f%%)" % [_current_hp.to_string_suffix(2), _max_hp.to_string_suffix(2), progress_pct]
var unit_balance: BigNumber = game_state.get_currency_amount_by_id(&"unit")
_damage_button.text = "Send Army (%s units)" % unit_balance.to_string_suffix(2)
_damage_button.disabled = unit_balance.mantissa <= 0.0
func _save_state() -> void:
if game_state == null:
return
var payload: Dictionary = {HP_KEY: _current_hp.serialize()}
game_state.set_external_save_data(SAVE_KEY, payload)
func _load_state() -> void:
if game_state == null:
return
var saved: Dictionary = game_state.get_external_save_data(SAVE_KEY)
if saved.has(HP_KEY):
var raw_hp: Variant = saved.get(HP_KEY)
if raw_hp is Dictionary:
_current_hp = BigNumber.deserialize(raw_hp)
if _current_hp.mantissa <= 0.0:
_current_hp = BigNumber.new(0.0, 0)
_is_destroyed = true
_victory_label.visible = true

View File

@@ -0,0 +1 @@
uid://blfof77kjp7x0

View File

@@ -0,0 +1,58 @@
[gd_scene format=3 uid="uid://d1cvlnd5vwjol"]
[ext_resource type="Script" uid="uid://blfof77kjp7x0" path="res://docs/gyms/tiny_sword/buildings/devil_idol/devil_idol.gd" id="1_script"]
[ext_resource type="Texture2D" uid="uid://rpedoq4102to" path="res://docs/gyms/tiny_sword/buildings/devil_idol/Castle.png" id="2_g7pcm"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_idol"]
size = Vector2(312, 198)
[node name="DevilIdol" type="Node2D" unique_id=797117647]
script = ExtResource("1_script")
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=661703335]
texture = ExtResource("2_g7pcm")
[node name="Area2D" type="Area2D" parent="." unique_id=1812195052]
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=1492517397]
position = Vector2(1, 22)
shape = SubResource("RectangleShape2D_idol")
[node name="IdolPanel" type="PanelContainer" parent="." unique_id=906038530]
z_index = 1
offset_left = -200.0
offset_top = -120.0
offset_right = 200.0
offset_bottom = -10.0
[node name="MarginContainer" type="MarginContainer" parent="IdolPanel" unique_id=1952977282]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="VBoxContainer" type="VBoxContainer" parent="IdolPanel/MarginContainer" unique_id=1214756176]
layout_mode = 2
theme_override_constants/separation = 8
[node name="TitleLabel" type="Label" parent="IdolPanel/MarginContainer/VBoxContainer" unique_id=1268978638]
layout_mode = 2
text = "Devil Idol"
[node name="HPValue" type="Label" parent="IdolPanel/MarginContainer/VBoxContainer" unique_id=529436307]
layout_mode = 2
text = "??? / ???"
[node name="DamageButton" type="Button" parent="IdolPanel/MarginContainer/VBoxContainer" unique_id=1565608485]
layout_mode = 2
disabled = true
text = "Send Army"
[node name="VictoryLabel" type="Label" parent="IdolPanel/MarginContainer/VBoxContainer" unique_id=169309796]
layout_mode = 2
text = "THE DEVIL IDOL HAS BEEN DESTROYED!"
horizontal_alignment = 1
[connection signal="mouse_entered" from="Area2D" to="." method="_on_area_2d_mouse_entered"]
[connection signal="mouse_exited" from="Area2D" to="." method="_on_area_2d_mouse_exited"]

View File

@@ -0,0 +1,25 @@
extends Node2D
const UNLOCK_GOAL_ID: StringName = &"gold_total_1300"
@onready var _sprite: Sprite2D = $Sprite2D
@onready var _area: Area2D = $Area2D
@onready var _game_state: LevelGameState = find_parent("LevelGameState")
func _ready() -> void:
_sprite.visible = false
_area.monitoring = false
_game_state.goal_completed.connect(_on_goal_completed)
if _game_state.is_goal_completed(UNLOCK_GOAL_ID):
_unlock()
func _on_goal_completed(goal_id: StringName) -> void:
if goal_id == UNLOCK_GOAL_ID:
_unlock()
func _unlock() -> void:
_sprite.visible = true
_area.monitoring = true

View File

@@ -0,0 +1 @@
uid://dnsblnjy82goo

View File

@@ -1,6 +1,7 @@
[gd_scene format=3 uid="uid://djedqovgngrx5"]
[ext_resource type="PackedScene" uid="uid://jeoiinukrrsp" path="res://core/generator/currency_generator.tscn" id="1_rvsna"]
[ext_resource type="Script" path="res://docs/gyms/tiny_sword/buildings/farm/farm.gd" id="1_script"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="2_djlc5"]
[ext_resource type="Resource" uid="uid://d08h51y0pnsnf" path="res://docs/gyms/tiny_sword/buildings/farm/farm_generator.tres" id="3_82rmq"]
[ext_resource type="Texture2D" uid="uid://dfdh8r03sj7qv" path="res://docs/gyms/tiny_sword/buildings/farm/House1.png" id="4_djlc5"]
@@ -10,6 +11,7 @@
size = Vector2(106, 157)
[node name="Farm" type="Node2D" unique_id=283335851]
script = ExtResource("1_script")
[node name="CurrencyGenerator" parent="." unique_id=967969064 node_paths=PackedStringArray("info_generator_container") instance=ExtResource("1_rvsna")]
currency = ExtResource("2_djlc5")
@@ -27,6 +29,7 @@ position = Vector2(0, -2.5)
shape = SubResource("RectangleShape2D_82rmq")
[node name="GeneratorContainer" parent="." unique_id=1451609580 node_paths=PackedStringArray("_generator") instance=ExtResource("5_82rmq")]
z_index = 1
visible = false
offset_left = 138.0
offset_top = -100.0

View File

@@ -1,13 +1,35 @@
extends Node2D
@onready var _generator_container: GeneratorPanel = $GeneratorContainer
const UNLOCK_GOAL_ID: StringName = &"gold_total_13k"
@onready var _generator_container: GeneratorPanel = $GeneratorContainer
@onready var _sprite: Sprite2D = $Sprite2D
@onready var _area: Area2D = $Area2D
@onready var _game_state: LevelGameState = find_parent("LevelGameState")
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
_generator_container.visible = false
_sprite.visible = false
_area.monitoring = false
_game_state.goal_completed.connect(_on_goal_completed)
if _game_state.is_goal_completed(UNLOCK_GOAL_ID):
_unlock()
func _on_area_2d_mouse_entered() -> void:
_generator_container.visible = true
func _on_area_2d_mouse_exited() -> void:
_generator_container.visible = false
func _on_goal_completed(goal_id: StringName) -> void:
if goal_id == UNLOCK_GOAL_ID:
_unlock()
func _unlock() -> void:
_sprite.visible = true
_area.monitoring = true

View File

@@ -29,6 +29,7 @@ position = Vector2(0.5, -2.5)
shape = SubResource("RectangleShape2D_v4aq3")
[node name="GeneratorContainer" parent="." unique_id=1451609580 node_paths=PackedStringArray("_generator") instance=ExtResource("5_rh3m6")]
z_index = 1
visible = false
offset_left = 128.0
offset_top = -100.0

View File

@@ -26,6 +26,7 @@ position = Vector2(-1, -11)
shape = SubResource("RectangleShape2D_sbi6i")
[node name="GeneratorContainer" parent="." unique_id=1451609580 node_paths=PackedStringArray("_generator") instance=ExtResource("2_lnbxc")]
z_index = 1
visible = false
offset_left = 127.0
offset_top = -103.0

View File

@@ -1,14 +1,37 @@
extends Node2D
@onready var _research_panel: ResearchPanel = $ResearchPanel
const RESEARCH_GOAL_ID: StringName = &"gold_350k_wood_20k"
@onready var _research_panel: ResearchPanel = $ResearchPanel
@onready var _sprite: Sprite2D = $Sprite2D
@onready var _area: Area2D = $Area2D
@onready var game_state: LevelGameState = find_parent("LevelGameState")
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
_research_panel.visible = false
_sprite.visible = false
_area.monitoring = false
if game_state:
game_state.goal_completed.connect(_on_goal_completed)
if game_state.is_goal_completed(RESEARCH_GOAL_ID):
_unlock()
func _on_area_2d_mouse_entered() -> void:
if game_state and game_state.is_goal_completed(RESEARCH_GOAL_ID):
_research_panel.visible = true
func _on_area_2d_mouse_exited() -> void:
_research_panel.visible = false
func _on_goal_completed(goal_id: StringName) -> void:
if goal_id == RESEARCH_GOAL_ID:
_unlock()
func _unlock() -> void:
_sprite.visible = true
_area.monitoring = true

View File

@@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://cj0sxoceqqyrm" path="res://docs/gyms/tiny_sword/buildings/monastery/monastery.gd" id="1_e17qd"]
[ext_resource type="Texture2D" uid="uid://dlmjd2berkb6v" path="res://docs/gyms/tiny_sword/buildings/monastery/Monastery.png" id="1_yyw6r"]
[ext_resource type="PackedScene" uid="uid://dd8roif0mqirl" path="res://core/research/research_panel.tscn" id="3_c6gi6"]
[ext_resource type="PackedScene" uid="uid://6d101h70mcx" path="res://core/research/research_panel.tscn" id="3_c6gi6"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_yyw6r"]
size = Vector2(160, 254)
@@ -20,11 +20,12 @@ position = Vector2(1, 16)
shape = SubResource("RectangleShape2D_yyw6r")
[node name="ResearchPanel" parent="." unique_id=1105274967 instance=ExtResource("3_c6gi6")]
z_index = 1
visible = false
offset_left = 78.0
offset_top = -98.0
offset_right = 478.0
offset_bottom = 128.0
offset_left = 59.0
offset_top = -87.0
offset_right = 459.0
offset_bottom = 139.0
[connection signal="mouse_entered" from="Area2D" to="." method="_on_area_2d_mouse_entered"]
[connection signal="mouse_exited" from="Area2D" to="." method="_on_area_2d_mouse_exited"]

View File

@@ -11,8 +11,9 @@
[ext_resource type="Resource" uid="uid://brctmnpmhjas6" path="res://docs/gyms/tiny_sword/currencies/mana_stone.tres" id="8_p3urk"]
[ext_resource type="Resource" uid="uid://t6du7gm2ywbi" path="res://docs/gyms/tiny_sword/currencies/cognite.tres" id="9_ejnoj"]
[ext_resource type="Resource" uid="uid://co4fiqgluwit0" path="res://docs/gyms/tiny_sword/currencies/philosoper_stone.tres" id="10_ejnoj"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/currencies/unit.tres" id="11_unit"]
[resource]
script = ExtResource("2_hmju3")
currencies = Array[ExtResource("1_501l6")]([ExtResource("2_ucsji"), ExtResource("3_7hx6u"), ExtResource("4_c77gh"), ExtResource("5_gyp05"), ExtResource("6_ascension"), ExtResource("7_vu05v"), ExtResource("8_p3urk"), ExtResource("9_ejnoj"), ExtResource("10_ejnoj")])
currencies = Array[ExtResource("1_501l6")]([ExtResource("2_ucsji"), ExtResource("3_7hx6u"), ExtResource("4_c77gh"), ExtResource("5_gyp05"), ExtResource("6_ascension"), ExtResource("7_vu05v"), ExtResource("8_p3urk"), ExtResource("9_ejnoj"), ExtResource("10_ejnoj"), ExtResource("11_unit")])
metadata/_custom_type_script = "uid://621tus0uvbrd"

View File

@@ -0,0 +1,10 @@
[gd_resource type="Resource" script_class="Currency" format=3]
[ext_resource type="Script" path="res://core/currency/currency.gd" id="1_script"]
[ext_resource type="Texture2D" path="res://sandbox/tiny_swords/UI Elements/UI Elements/Icons/Icon_05.png" id="2_icon"]
[resource]
script = ExtResource("1_script")
id = &"unit"
display_name = "Unit"
icon = ExtResource("2_icon")

View File

@@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://bex2hsenbr54e"]
[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_req"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_gold"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="3_food"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="4_wood"]
[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="5_goal"]
[sub_resource type="Resource" id="Resource_gold"]
script = ExtResource("1_req")
currency = ExtResource("2_gold")
amount_mantissa = 2.0
amount_exponent = 6
[sub_resource type="Resource" id="Resource_food"]
script = ExtResource("1_req")
currency = ExtResource("3_food")
amount_mantissa = 2.0
amount_exponent = 5
[sub_resource type="Resource" id="Resource_wood"]
script = ExtResource("1_req")
currency = ExtResource("4_wood")
amount_mantissa = 2.0
amount_exponent = 5
[resource]
script = ExtResource("5_goal")
id = &"gold_2M_food_200k_wood_200k"
requirements = Array[ExtResource("1_req")]([SubResource("Resource_gold"), SubResource("Resource_food"), SubResource("Resource_wood")])

View File

@@ -0,0 +1,30 @@
[gd_resource type="Resource" script_class="GoalData" format=3 uid="uid://bn4gm4sss85d0"]
[ext_resource type="Script" uid="uid://r4js5eajolio" path="res://core/goals/goal_requirement_data.gd" id="1_req"]
[ext_resource type="Resource" uid="uid://w4u1hddplb4e" path="res://docs/gyms/tiny_sword/currencies/gold.tres" id="2_gold"]
[ext_resource type="Resource" uid="uid://bxg2au0ijp242" path="res://docs/gyms/tiny_sword/currencies/food.tres" id="3_food"]
[ext_resource type="Resource" uid="uid://bgsk8h4w80h45" path="res://docs/gyms/tiny_sword/currencies/wood.tres" id="4_wood"]
[ext_resource type="Script" uid="uid://dx4kmh33rrkpy" path="res://core/goals/goal_data.gd" id="5_goal"]
[sub_resource type="Resource" id="Resource_gold"]
script = ExtResource("1_req")
currency = ExtResource("2_gold")
amount_mantissa = 5.0
amount_exponent = 7
[sub_resource type="Resource" id="Resource_food"]
script = ExtResource("1_req")
currency = ExtResource("3_food")
amount_mantissa = 5.0
amount_exponent = 6
[sub_resource type="Resource" id="Resource_wood"]
script = ExtResource("1_req")
currency = ExtResource("4_wood")
amount_mantissa = 5.0
amount_exponent = 6
[resource]
script = ExtResource("5_goal")
id = &"gold_50M_food_5M_wood_5M"
requirements = Array[ExtResource("1_req")]([SubResource("Resource_gold"), SubResource("Resource_food"), SubResource("Resource_wood")])

View File

@@ -6,7 +6,9 @@
[ext_resource type="Resource" uid="uid://cts0407h130d6" path="res://docs/gyms/tiny_sword/goals/gold_total_1300_goal.tres" id="3_wb626"]
[ext_resource type="Resource" uid="uid://bo463s6jt0ep7" path="res://docs/gyms/tiny_sword/goals/gold_total_13k_goal.tres" id="4_3oaoj"]
[ext_resource type="Resource" uid="uid://b6gb6vk0kgxnw" path="res://docs/gyms/tiny_sword/goals/gold_350k_wood_20k_goal.tres" id="5_or88d"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/goals/gold_2M_food_200k_wood_200k_goal.tres" id="6_alchemy"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/goals/gold_50M_food_5M_wood_5M_goal.tres" id="7_barracks"]
[resource]
script = ExtResource("2_84pbn")
goals = Array[ExtResource("1_6p6ue")]([ExtResource("2_xvtf2"), ExtResource("3_wb626"), ExtResource("4_3oaoj"), ExtResource("5_or88d")])
goals = Array[ExtResource("1_6p6ue")]([ExtResource("2_xvtf2"), ExtResource("3_wb626"), ExtResource("4_3oaoj"), ExtResource("5_or88d"), ExtResource("6_alchemy"), ExtResource("7_barracks")])

View File

@@ -0,0 +1,13 @@
[gd_resource type="Resource" script_class="PrestigeBuffNode" format=3]
[ext_resource type="Script" path="res://core/prestige/prestige_buff_node.gd" id="1_script"]
[resource]
script = ExtResource("1_script")
id = &"barracks_boost"
display_name = "Martial Prowess"
description = "Permanently increases barracks unit production."
parent_ids = Array[StringName]([&"wood_boost"])
effect_value = 2.0
target_id = &"barracks"
cost_mantissa = 5.0

View File

@@ -1,10 +1,12 @@
[gd_resource type="Resource" script_class="PrestigeBuffCatalogue" format=3]
[gd_resource type="Resource" script_class="PrestigeBuffCatalogue" format=3 uid="uid://dpt3j1c2ndgkj"]
[ext_resource type="Script" path="res://core/prestige/prestige_buff_node.gd" id="1_node"]
[ext_resource type="Script" path="res://core/prestige/prestige_buff_catalogue.gd" id="2_cat"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres" id="3_gold"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres" id="4_farm"]
[ext_resource type="Script" uid="uid://mjyig8xlgki0" path="res://core/prestige/prestige_buff_node.gd" id="1_node"]
[ext_resource type="Script" uid="uid://bjcvnyh2bc6l5" path="res://core/prestige/prestige_buff_catalogue.gd" id="2_cat"]
[ext_resource type="Resource" uid="uid://vl3ot4t876pc" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres" id="3_gold"]
[ext_resource type="Resource" uid="uid://cvogxo0msis2w" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres" id="4_farm"]
[ext_resource type="Resource" uid="uid://bif136o485oqh" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_wood_boost.tres" id="5_wood"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_barracks_boost.tres" id="6_barracks"]
[resource]
script = ExtResource("2_cat")
nodes = Array[ExtResource("1_node")]([ExtResource("3_gold"), ExtResource("4_farm")])
nodes = Array[ExtResource("1_node")]([ExtResource("3_gold"), ExtResource("4_farm"), ExtResource("5_wood"), ExtResource("6_barracks")])

View File

@@ -10,4 +10,5 @@ description = "Permanently increases farm food production."
parent_ids = Array[StringName]([&"gold_boost"])
effect_value = 2.0
target_id = &"farm"
cost_mantissa = 2.0
x_position = 1.0

View File

@@ -5,9 +5,13 @@
[ext_resource type="PackedScene" uid="uid://bqk8rwia7lpbg" path="res://core/prestige/prestige_buff_graph_tile.tscn" id="3_tile"]
[ext_resource type="Resource" uid="uid://vl3ot4t876pc" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_gold_boost.tres" id="4_gold"]
[ext_resource type="Resource" uid="uid://cvogxo0msis2w" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_farm_boost.tres" id="5_farm"]
[ext_resource type="Resource" uid="uid://bif136o485oqh" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_wood_boost.tres" id="6_pdyju"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_barracks_boost.tres" id="7_x62av"]
[node name="PrestigeBuffGraphPanel" type="PanelContainer" unique_id=947835716]
custom_minimum_size = Vector2(600, 400)
offset_right = 857.0
offset_bottom = 632.0
script = ExtResource("1_panel")
[node name="MarginContainer" type="MarginContainer" parent="." unique_id=829451203]
@@ -56,14 +60,31 @@ script = ExtResource("2_overlay")
[node name="GoldBoostTile" parent="MarginContainer/VBoxContainer/TilesArea" unique_id=756192834 instance=ExtResource("3_tile")]
layout_mode = 0
offset_top = 27.0
offset_right = 220.0
offset_bottom = 100.0
offset_bottom = 127.0
prestige_buff = ExtResource("4_gold")
[node name="FarmBoostTile" parent="MarginContainer/VBoxContainer/TilesArea" unique_id=619283475 instance=ExtResource("3_tile")]
layout_mode = 0
offset_left = 3.0
offset_top = 121.0
offset_top = 198.0
offset_right = 223.0
offset_bottom = 221.0
offset_bottom = 298.0
prestige_buff = ExtResource("5_farm")
[node name="FarmBoostTile2" parent="MarginContainer/VBoxContainer/TilesArea" unique_id=582396224 instance=ExtResource("3_tile")]
layout_mode = 0
offset_left = 317.0
offset_top = 198.0
offset_right = 537.0
offset_bottom = 298.0
prestige_buff = ExtResource("6_pdyju")
[node name="FarmBoostTile3" parent="MarginContainer/VBoxContainer/TilesArea" unique_id=818264061 instance=ExtResource("3_tile")]
layout_mode = 0
offset_left = 317.0
offset_top = 382.0
offset_right = 537.0
offset_bottom = 482.0
prestige_buff = ExtResource("7_x62av")

View File

@@ -10,3 +10,4 @@ description = "Permanently increases forestry production."
parent_ids = Array[StringName]([&"farm_boost"])
effect_value = 2.0
target_id = &"forestry"
cost_mantissa = 3.0

View File

@@ -6,4 +6,6 @@
script = ExtResource("1_3tg3a")
id = &"ascension"
prestige_currency_id = &"ascension"
basis = 3
source_currency_id = &"gold"
basis = 1
threshold_mantissa = 5.0

View File

@@ -8,7 +8,7 @@
[ext_resource type="Resource" uid="uid://cgt1mjir1v4br" path="res://docs/gyms/tiny_sword/goals/ts_goal_catalogue.tres" id="4_x77df"]
[ext_resource type="Resource" uid="uid://umi37hotcq8m" path="res://docs/gyms/tiny_sword/research/ts_research_catalogue.tres" id="5_v0pty"]
[ext_resource type="Script" uid="uid://srkiu4qe8s2m" path="res://core/prestige/prestige_manager.gd" id="5_x77df"]
[ext_resource type="Resource" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_catalogue.tres" id="6_asc"]
[ext_resource type="Resource" uid="uid://dpt3j1c2ndgkj" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_catalogue.tres" id="6_asc"]
[ext_resource type="Resource" uid="uid://dwmfvmusfskk6" path="res://docs/gyms/tiny_sword/prestige/primary_prestige.tres" id="6_xnhlc"]
[ext_resource type="PackedScene" path="res://docs/gyms/tiny_sword/currency_panel.tscn" id="9_cpnl"]
[ext_resource type="PackedScene" uid="uid://djedqovgngrx5" path="res://docs/gyms/tiny_sword/buildings/farm/farm.tscn" id="10_1lv5i"]
@@ -17,6 +17,8 @@
[ext_resource type="PackedScene" uid="uid://bf8lqbexvnx6e" path="res://docs/gyms/tiny_sword/buildings/monastery/monastery.tscn" id="13_no27p"]
[ext_resource type="PackedScene" uid="uid://rejxvjwybkll" path="res://sandbox/tiny_swords/Terrain/Resources/Wood/Trees/tree_1.tscn" id="14_0cs5o"]
[ext_resource type="PackedScene" uid="uid://bp5ng4vu4ot4a" path="res://docs/gyms/tiny_sword/buildings/alchemy_tower/alchemy_tower.tscn" id="14_hum8s"]
[ext_resource type="PackedScene" uid="uid://cyvijgwj6l8vg" path="res://docs/gyms/tiny_sword/buildings/barracks/barracks.tscn" id="15_barracks"]
[ext_resource type="PackedScene" uid="uid://d1cvlnd5vwjol" path="res://docs/gyms/tiny_sword/buildings/devil_idol/devil_idol.tscn" id="16_idol"]
[ext_resource type="Script" uid="uid://b6systw05frh0" path="res://core/edge_scroll_camera.gd" id="17_x77df"]
[ext_resource type="PackedScene" uid="uid://dprbgokqitaav" path="res://docs/gyms/tiny_sword/prestige/prestige_buff_graph_panel.tscn" id="18_pnl"]
@@ -114,6 +116,12 @@ position = Vector2(105, 920)
[node name="AlchemyTower" parent="LevelGameState/World" unique_id=51852160 instance=ExtResource("14_hum8s")]
position = Vector2(1239, 775)
[node name="Barracks" parent="LevelGameState/World" unique_id=765432109 instance=ExtResource("15_barracks")]
position = Vector2(1597, 900)
[node name="DevilIdol" parent="LevelGameState/World" unique_id=876543210 instance=ExtResource("16_idol")]
position = Vector2(873, 1289)
[node name="Camera2D" type="Camera2D" parent="." unique_id=1494908331]
position = Vector2(5, 0)
offset = Vector2(960, 540)

View File

@@ -6,7 +6,7 @@ var _game_root: Node = null
func _ready() -> void:
await run()
pass # run() is called by the test runner; avoid double-execution.
func run() -> void:
@@ -118,10 +118,17 @@ func _test_purchase_flow() -> void:
var scene: PackedScene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
_game_root = scene.instantiate()
# Use a unique save path to avoid contamination from previous test runs.
var save_path: String = "user://test_ascension_%d.json" % randi()
DirAccess.remove_absolute(ProjectSettings.globalize_path(save_path))
var gs: LevelGameState = _game_root.get_node("LevelGameState")
gs.save_file_path = save_path
add_child(_game_root)
await _wait()
var gs: LevelGameState = _game_root.find_child("LevelGameState")
gs = _game_root.find_child("LevelGameState")
if gs == null:
print("[ERROR] LevelGameState not found")
return
@@ -142,9 +149,9 @@ func _test_purchase_flow() -> void:
TestUtils.assert_false(gs.is_prestige_buff_unlocked(&"gold_boost"), "gold_boost starts locked")
TestUtils.assert_false(gs.is_prestige_buff_unlocked(&"farm_boost"), "farm_boost starts locked")
# Check can_purchase (both are root nodes with no parents, should be purchaseable)
# gold_boost is a root node (no parents), farm_boost requires gold_boost as parent.
TestUtils.assert_true(gs.can_purchase_prestige_buff(&"gold_boost"), "gold_boost is purchasable")
TestUtils.assert_true(gs.can_purchase_prestige_buff(&"farm_boost"), "farm_boost is purchasable")
TestUtils.assert_false(gs.can_purchase_prestige_buff(&"farm_boost"), "farm_boost is NOT purchasable (parent gold_boost not yet unlocked)")
# Check cost
var cost: BigNumber = gs.get_prestige_buff_cost(&"gold_boost")

View File

@@ -6,7 +6,6 @@ var _game_root: Node = null
func _ready() -> void:
if not Engine.is_editor_hint():
test_goals_prestige_reset()
get_tree().quit()
func test_goals_prestige_reset() -> void:
print("\n=== TEST: Goals Prestige Reset ===\n")
@@ -15,6 +14,13 @@ func test_goals_prestige_reset() -> void:
# Load the tiny_sword scene directly
var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
_game_root = scene.instantiate()
# Use a unique save path to avoid contaminating the real game's save.
var save_path: String = "user://test_goals_prestige_%d.json" % randi()
DirAccess.remove_absolute(ProjectSettings.globalize_path(save_path))
var gs: LevelGameState = _game_root.get_node("LevelGameState")
gs.save_file_path = save_path
add_child(_game_root)
print("[TARGET] children_after_load: %d" % get_child_count())

View File

@@ -5,8 +5,7 @@ var failed: int = 0
var _game_root: Node = null
func _ready():
await run()
# Don't quit here - let test_runner handle it
pass # run() is called by the test runner; avoid double-execution.
func run():
print("\n=== TEST: Gold Mine Click ===\n")
@@ -15,34 +14,62 @@ func run():
# Load tiny_sword scene
var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
_game_root = scene.instantiate()
# Use a unique save path to avoid contamination from previous test runs.
var save_path: String = "user://test_gold_mine_%d.json" % randi()
DirAccess.remove_absolute(ProjectSettings.globalize_path(save_path))
var gs: LevelGameState = _game_root.get_node("LevelGameState")
gs.save_file_path = save_path
add_child(_game_root)
await _wait()
var game_state: LevelGameState = _game_root.find_child("LevelGameState")
var gold_mine = _game_root.find_child("GoldMine") as CurrencyGenerator
var gold_mine_node: Node = _game_root.find_child("GoldMine")
if game_state == null:
print("[ERROR] LevelGameState not found")
_print_summary()
return
if gold_mine == null:
if gold_mine_node == null:
print("[ERROR] GoldMine node not found")
_print_summary()
return
# Record initial gold
var initial_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold")
var gold_mine: CurrencyGenerator = gold_mine_node.find_child("CurrencyGenerator") as CurrencyGenerator
if gold_mine == null:
print("[ERROR] CurrencyGenerator not found under GoldMine")
_print_summary()
return
# Verify game_state is reachable and generator is available
TestUtils.assert_not_null(gold_mine.game_state, "game_state is not null")
TestUtils.assert_true(gold_mine.is_available_to_player(), "Gold Mine is available to player")
# Record initial gold (copy to avoid BigNumber reference sharing)
var initial_gold: BigNumber = BigNumber.new(
game_state.get_currency_amount_by_id(&"gold").mantissa,
game_state.get_currency_amount_by_id(&"gold").exponent
)
print("[TARGET] initial_gold: %s" % initial_gold.to_string_suffix(2))
# Temporarily set press_buys_generator to false so _on_pressed grants click currency
# (Gold Mine defaults to true which would try to buy with workers instead)
var original_press_buys: bool = gold_mine.press_buys_generator
gold_mine.press_buys_generator = false
# Simulate clicking the generator
gold_mine._on_pressed()
await _wait()
# Validate gold increased
var final_gold: BigNumber = game_state.get_currency_amount_by_id(&"gold")
# Validate gold increased (copy to avoid BigNumber reference sharing)
var final_gold: BigNumber = BigNumber.new(
game_state.get_currency_amount_by_id(&"gold").mantissa,
game_state.get_currency_amount_by_id(&"gold").exponent
)
print("[TARGET] final_gold: %s" % final_gold.to_string_suffix(2))
TestUtils.assert_greater_than(

View File

@@ -5,9 +5,7 @@ var failed: int = 0
var _game_root: Node = null
func _ready():
await run()
# Let test_runner handle exit
pass
pass # run() is called by the test runner; avoid double-execution.
func run():
print("\n=== TEST: Prestige Mechanics ===\n")
@@ -15,6 +13,13 @@ func run():
var scene = load("res://docs/gyms/tiny_sword/tiny_sword.tscn")
_game_root = scene.instantiate()
# Use a unique save path to avoid contaminating the real game's save.
var save_path: String = "user://test_prestige_%d.json" % randi()
DirAccess.remove_absolute(ProjectSettings.globalize_path(save_path))
var gs: LevelGameState = _game_root.get_node("LevelGameState")
gs.save_file_path = save_path
add_child(_game_root)
await _wait()