Combat base added
This commit is contained in:
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(find:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
205
docs/superpowers/plans/2026-04-02-combat-system.md
Normal file
205
docs/superpowers/plans/2026-04-02-combat-system.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# Combat System Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Create a stateless combat system that produces pre-combat proposals and resolves attacks between two units.
|
||||||
|
|
||||||
|
**Architecture:** A `CombatProposal` resource holds snapshot stats for attacker and defender. A `CombatSystem` node exposes `create_proposal()` to build proposals and `apply_proposal()` to resolve combat, modifying unit HP directly. The system is added to the strategy phase scene tree.
|
||||||
|
|
||||||
|
**Tech Stack:** Godot 4.6, GDScript
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Responsibility |
|
||||||
|
|------|---------------|
|
||||||
|
| `resources/resource_definitions/combat_proposal.gd` | CombatProposal resource with CombatantStats inner class — holds unit references and snapshot stats |
|
||||||
|
| `nodes/combat_system.gd` | CombatSystem node — `create_proposal()` and `apply_proposal()` methods |
|
||||||
|
| `scenes/strategy_phase.tscn` | Scene tree — add CombatSystem node |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Create the CombatProposal resource
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `resources/resource_definitions/combat_proposal.gd`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the CombatantStats inner class and CombatProposal resource**
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# resources/resource_definitions/combat_proposal.gd
|
||||||
|
class_name CombatProposal extends Resource
|
||||||
|
|
||||||
|
class CombatantStats:
|
||||||
|
var unit: Unit
|
||||||
|
var hp: int
|
||||||
|
var sp: int
|
||||||
|
var hit: int
|
||||||
|
var atk: int
|
||||||
|
var def: int
|
||||||
|
var spd: int
|
||||||
|
|
||||||
|
var attacker: CombatantStats
|
||||||
|
var defender: CombatantStats
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify the file loads without errors**
|
||||||
|
|
||||||
|
Open the Godot editor and check the Output panel for parse errors. Confirm `CombatProposal` appears as a recognized class by typing it in any script's autocomplete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Create the CombatSystem node
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `nodes/combat_system.gd`
|
||||||
|
- Reference: `resources/resource_definitions/combat_proposal.gd`
|
||||||
|
- Reference: `resources/resource_definitions/unit_stats.gd`
|
||||||
|
- Reference: `nodes/unit.gd`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create combat_system.gd with create_proposal**
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# nodes/combat_system.gd
|
||||||
|
class_name CombatSystem extends Node
|
||||||
|
|
||||||
|
func create_proposal(attacker: Unit, defender: Unit) -> CombatProposal:
|
||||||
|
var proposal := CombatProposal.new()
|
||||||
|
|
||||||
|
proposal.attacker = _snapshot(attacker, defender)
|
||||||
|
proposal.defender = _snapshot(defender, attacker)
|
||||||
|
|
||||||
|
return proposal
|
||||||
|
|
||||||
|
func _snapshot(unit: Unit, opponent: Unit) -> CombatProposal.CombatantStats:
|
||||||
|
var stats := CombatProposal.CombatantStats.new()
|
||||||
|
stats.unit = unit
|
||||||
|
stats.hp = unit.current_stats.current_hp
|
||||||
|
stats.sp = unit.current_stats.current_sp
|
||||||
|
stats.hit = unit.current_stats.hit - opponent.current_stats.eva
|
||||||
|
stats.atk = unit.current_stats.phys_atk
|
||||||
|
stats.def = unit.current_stats.phys_def
|
||||||
|
stats.spd = unit.current_stats.spd
|
||||||
|
return stats
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add apply_proposal method**
|
||||||
|
|
||||||
|
Append to `nodes/combat_system.gd`:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func apply_proposal(proposal: CombatProposal) -> void:
|
||||||
|
var atk_stats := proposal.attacker
|
||||||
|
var def_stats := proposal.defender
|
||||||
|
var atk_unit := atk_stats.unit
|
||||||
|
var def_unit := def_stats.unit
|
||||||
|
|
||||||
|
# Attacker strikes
|
||||||
|
var atk_roll := randi_range(1, 100)
|
||||||
|
if atk_roll <= atk_stats.hit:
|
||||||
|
var damage := maxi(atk_stats.atk - def_stats.def, 0)
|
||||||
|
def_unit.current_stats.current_hp -= damage
|
||||||
|
|
||||||
|
# Counterattack if defender survives
|
||||||
|
if def_unit.current_stats.current_hp > 0:
|
||||||
|
var def_roll := randi_range(1, 100)
|
||||||
|
if def_roll <= def_stats.hit:
|
||||||
|
var damage := maxi(def_stats.atk - atk_stats.def, 0)
|
||||||
|
atk_unit.current_stats.current_hp -= damage
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verify the file loads without errors**
|
||||||
|
|
||||||
|
Open the Godot editor and check the Output panel for parse errors on `combat_system.gd`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Add CombatSystem to the strategy phase scene
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scenes/strategy_phase.tscn`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add CombatSystem node and script reference to the scene**
|
||||||
|
|
||||||
|
Add a new `ext_resource` entry for the combat_system.gd script and a new node to the scene file. Append after the existing ext_resources and add the node as a child of the root `CombatTest` node:
|
||||||
|
|
||||||
|
New ext_resource (use the next available id, `6_xxxxx` — the exact suffix will be assigned):
|
||||||
|
```
|
||||||
|
[ext_resource type="Script" path="res://nodes/combat_system.gd" id="6_combat"]
|
||||||
|
```
|
||||||
|
|
||||||
|
New node (add after the PlayerController node block):
|
||||||
|
```
|
||||||
|
[node name="CombatSystem" type="Node" parent="."]
|
||||||
|
script = ExtResource("6_combat")
|
||||||
|
```
|
||||||
|
|
||||||
|
The resulting scene tree order:
|
||||||
|
```
|
||||||
|
CombatTest (Node2D)
|
||||||
|
├─ CombatUI
|
||||||
|
├─ CombatMap
|
||||||
|
├─ PlayerController
|
||||||
|
├─ CombatSystem <-- new
|
||||||
|
├─ Camera2D
|
||||||
|
└─ AudioStreamPlayer
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify in the Godot editor**
|
||||||
|
|
||||||
|
Open `scenes/strategy_phase.tscn` in the editor. Confirm:
|
||||||
|
- CombatSystem node appears in the scene tree
|
||||||
|
- No errors in the Output panel
|
||||||
|
- The scene runs without crashes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Manual integration verification
|
||||||
|
|
||||||
|
- [ ] **Step 1: Verify create_proposal works**
|
||||||
|
|
||||||
|
Temporarily add the following to `CombatSystem._ready()` to test proposal creation (remove after verification):
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func _ready() -> void:
|
||||||
|
# Temporary test — remove after verification
|
||||||
|
var units := get_tree().get_nodes_in_group("units")
|
||||||
|
if units.size() >= 2:
|
||||||
|
var proposal := create_proposal(units[0], units[1])
|
||||||
|
print("Proposal created:")
|
||||||
|
print(" Attacker: ", proposal.attacker.unit.current_info.name,
|
||||||
|
" HP=", proposal.attacker.hp,
|
||||||
|
" ATK=", proposal.attacker.atk,
|
||||||
|
" DEF=", proposal.attacker.def,
|
||||||
|
" HIT=", proposal.attacker.hit)
|
||||||
|
print(" Defender: ", proposal.defender.unit.current_info.name,
|
||||||
|
" HP=", proposal.defender.hp,
|
||||||
|
" ATK=", proposal.defender.atk,
|
||||||
|
" DEF=", proposal.defender.def,
|
||||||
|
" HIT=", proposal.defender.hit)
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the scene with at least 2 units deployed. Check the Output panel for correct stat snapshots.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify apply_proposal works**
|
||||||
|
|
||||||
|
Extend the temporary `_ready()` to also apply the proposal:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
print("Before combat: Attacker HP=", proposal.attacker.unit.current_stats.current_hp,
|
||||||
|
" Defender HP=", proposal.defender.unit.current_stats.current_hp)
|
||||||
|
apply_proposal(proposal)
|
||||||
|
print("After combat: Attacker HP=", proposal.attacker.unit.current_stats.current_hp,
|
||||||
|
" Defender HP=", proposal.defender.unit.current_stats.current_hp)
|
||||||
|
```
|
||||||
|
|
||||||
|
Run multiple times (results will vary due to hit rolls). Confirm:
|
||||||
|
- Defender HP decreases when attacker hits
|
||||||
|
- Attacker HP decreases when defender counterattacks
|
||||||
|
- HP never increases (no negative damage)
|
||||||
|
- If defender dies (HP <= 0), no counterattack occurs
|
||||||
|
|
||||||
|
- [ ] **Step 3: Remove temporary test code**
|
||||||
|
|
||||||
|
Delete the entire `_ready()` method from `combat_system.gd`.
|
||||||
87
docs/superpowers/specs/2026-04-02-combat-system-design.md
Normal file
87
docs/superpowers/specs/2026-04-02-combat-system-design.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Combat System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A stateless CombatSystem node that creates pre-combat proposals and applies combat resolution between two units. The system encapsulates all combat stat calculation so other systems don't need to know proposal logic.
|
||||||
|
|
||||||
|
## CombatProposal Resource
|
||||||
|
|
||||||
|
`resources/resource_definitions/combat_proposal.gd`
|
||||||
|
|
||||||
|
A Resource containing two sides — attacker and defender. Each side holds:
|
||||||
|
|
||||||
|
- A reference to the `Unit` node
|
||||||
|
- Pre-combat snapshot stats via an inner class `CombatantStats`:
|
||||||
|
- `hp: int` — from `current_stats.current_hp`
|
||||||
|
- `sp: int` — from `current_stats.current_sp`
|
||||||
|
- `hit: int` — calculated as `own hit - opponent's eva`
|
||||||
|
- `atk: int` — from `current_stats.phys_atk`
|
||||||
|
- `def: int` — from `current_stats.phys_def`
|
||||||
|
- `spd: int` — from `current_stats.spd`
|
||||||
|
|
||||||
|
### Stat Calculation
|
||||||
|
|
||||||
|
For the **attacker** side:
|
||||||
|
- `atk` = attacker's `phys_atk`
|
||||||
|
- `def` = attacker's `phys_def`
|
||||||
|
- `hit` = attacker's `hit` - defender's `eva`
|
||||||
|
- `hp`, `sp`, `spd` copied directly from attacker's `current_stats`
|
||||||
|
|
||||||
|
For the **defender** side:
|
||||||
|
- `atk` = defender's `phys_atk`
|
||||||
|
- `def` = defender's `phys_def`
|
||||||
|
- `hit` = defender's `hit` - attacker's `eva`
|
||||||
|
- `hp`, `sp`, `spd` copied directly from defender's `current_stats`
|
||||||
|
|
||||||
|
In the future, different attacks may swap between physical and magic stats, but for now only physical stats are used.
|
||||||
|
|
||||||
|
## CombatSystem Node
|
||||||
|
|
||||||
|
`nodes/combat_system.gd`
|
||||||
|
|
||||||
|
A `Node` added to the strategy phase scene tree. Stateless — operates purely on units passed in.
|
||||||
|
|
||||||
|
### `create_proposal(attacker: Unit, defender: Unit) -> CombatProposal`
|
||||||
|
|
||||||
|
- Reads both units' `current_stats`
|
||||||
|
- Builds a `CombatProposal` with snapshot stats for each side
|
||||||
|
- Applies cross-calculations (hit - eva)
|
||||||
|
- Returns the proposal without modifying any unit state
|
||||||
|
|
||||||
|
### `apply_proposal(proposal: CombatProposal) -> void`
|
||||||
|
|
||||||
|
1. **Attacker strikes:** Roll random int 1–100. If roll <= attacker's calculated `hit`, apply `max(attacker.atk - defender.def, 0)` damage to the defender Unit's `current_stats.current_hp` (modifies the actual unit, not the snapshot).
|
||||||
|
2. **Counterattack:** If the defender Unit's `current_stats.current_hp > 0` after the attack, roll 1–100 for defender. If roll <= defender's calculated `hit`, apply `max(defender.atk - attacker.def, 0)` damage to the attacker Unit's `current_stats.current_hp`.
|
||||||
|
|
||||||
|
- Damage has a floor of 0 (no negative damage / healing).
|
||||||
|
- Range is ignored for now. In the future, counterattack will depend on whether the defender has skills at appropriate range (defaulting to a "defend" action if not).
|
||||||
|
|
||||||
|
## Scene Integration
|
||||||
|
|
||||||
|
The CombatSystem node is added to `scenes/strategy_phase.tscn` as a sibling of PlayerController, CombatMap, etc. No exports needed.
|
||||||
|
|
||||||
|
```
|
||||||
|
CombatTest (Node2D)
|
||||||
|
├─ CombatUI
|
||||||
|
├─ CombatMap
|
||||||
|
├─ PlayerController
|
||||||
|
├─ CombatSystem <-- new
|
||||||
|
├─ Camera2D
|
||||||
|
└─ AudioStreamPlayer
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `resources/resource_definitions/combat_proposal.gd` | New — CombatProposal resource with CombatantStats inner class |
|
||||||
|
| `nodes/combat_system.gd` | New — CombatSystem node with `create_proposal` and `apply_proposal` |
|
||||||
|
| `scenes/strategy_phase.tscn` | Add CombatSystem node to scene tree |
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Magic attack/defense selection (future: different attack types swap stats)
|
||||||
|
- Range-based counterattack eligibility (future: defend action when no skills in range)
|
||||||
|
- Critical hits
|
||||||
|
- UI for displaying the combat proposal
|
||||||
|
- Animations or visual feedback for combat resolution
|
||||||
@@ -25,11 +25,9 @@ func _ready() -> void:
|
|||||||
func snap_to_grid(pos: Vector2) -> Vector2:
|
func snap_to_grid(pos: Vector2) -> Vector2:
|
||||||
return Vector2(floorf(pos.x / TILE_SIZE), floorf(pos.y / TILE_SIZE)) * TILE_SIZE
|
return Vector2(floorf(pos.x / TILE_SIZE), floorf(pos.y / TILE_SIZE)) * TILE_SIZE
|
||||||
|
|
||||||
|
|
||||||
func world_to_coords(pos: Vector2) -> Vector2i:
|
func world_to_coords(pos: Vector2) -> Vector2i:
|
||||||
return Vector2i(snap_to_grid(pos) / TILE_SIZE)
|
return Vector2i(snap_to_grid(pos) / TILE_SIZE)
|
||||||
|
|
||||||
|
|
||||||
func coords_to_world(coords: Vector2i) -> Vector2:
|
func coords_to_world(coords: Vector2i) -> Vector2:
|
||||||
return Vector2(coords) * TILE_SIZE
|
return Vector2(coords) * TILE_SIZE
|
||||||
|
|
||||||
|
|||||||
50
nodes/combat_system.gd
Normal file
50
nodes/combat_system.gd
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
class_name CombatSystem extends Node
|
||||||
|
|
||||||
|
func create_proposal(attacker: Unit, defender: Unit) -> CombatProposal:
|
||||||
|
var proposal := CombatProposal.new()
|
||||||
|
|
||||||
|
proposal.attacker = _snapshot(attacker, defender)
|
||||||
|
proposal.defender = _snapshot(defender, attacker)
|
||||||
|
|
||||||
|
return proposal
|
||||||
|
|
||||||
|
func _snapshot(unit: Unit, opponent: Unit) -> CombatProposal.CombatantStats:
|
||||||
|
var stats := CombatProposal.CombatantStats.new()
|
||||||
|
stats.unit = unit
|
||||||
|
stats.hp = unit.current_stats.current_hp
|
||||||
|
stats.sp = unit.current_stats.current_sp
|
||||||
|
stats.hit = unit.current_stats.hit - opponent.current_stats.eva
|
||||||
|
stats.atk = unit.current_stats.phys_atk
|
||||||
|
stats.def = unit.current_stats.phys_def
|
||||||
|
stats.spd = unit.current_stats.spd
|
||||||
|
return stats
|
||||||
|
|
||||||
|
func process_combat(attacker: Unit, defender: Unit) -> void:
|
||||||
|
var proposal := create_proposal(attacker, defender)
|
||||||
|
var atk_name := attacker.current_info.name
|
||||||
|
var def_name := defender.current_info.name
|
||||||
|
print("=== Combat: %s vs %s ===" % [atk_name, def_name])
|
||||||
|
print(" %s — HP:%d ATK:%d DEF:%d HIT:%d" % [atk_name, proposal.attacker.hp, proposal.attacker.atk, proposal.attacker.def, proposal.attacker.hit])
|
||||||
|
print(" %s — HP:%d ATK:%d DEF:%d HIT:%d" % [def_name, proposal.defender.hp, proposal.defender.atk, proposal.defender.def, proposal.defender.hit])
|
||||||
|
apply_proposal(proposal)
|
||||||
|
print(" Result: %s HP=%d, %s HP=%d" % [atk_name, attacker.current_stats.current_hp, def_name, defender.current_stats.current_hp])
|
||||||
|
|
||||||
|
|
||||||
|
func apply_proposal(proposal: CombatProposal) -> void:
|
||||||
|
var atk_stats := proposal.attacker
|
||||||
|
var def_stats := proposal.defender
|
||||||
|
var atk_unit := atk_stats.unit
|
||||||
|
var def_unit := def_stats.unit
|
||||||
|
|
||||||
|
# Attacker strikes
|
||||||
|
var atk_roll := randi_range(1, 100)
|
||||||
|
if atk_roll <= atk_stats.hit:
|
||||||
|
var damage := maxi(atk_stats.atk - def_stats.def, 0)
|
||||||
|
def_unit.current_stats.current_hp -= damage
|
||||||
|
|
||||||
|
# Counterattack if defender survives
|
||||||
|
if def_unit.current_stats.current_hp > 0:
|
||||||
|
var def_roll := randi_range(1, 100)
|
||||||
|
if def_roll <= def_stats.hit:
|
||||||
|
var damage := maxi(def_stats.atk - atk_stats.def, 0)
|
||||||
|
atk_unit.current_stats.current_hp -= damage
|
||||||
1
nodes/combat_system.gd.uid
Normal file
1
nodes/combat_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cf4ivrcbky0s3
|
||||||
@@ -4,6 +4,8 @@ const SPEED = 192.0
|
|||||||
|
|
||||||
@export var dl_map: CombatMap
|
@export var dl_map: CombatMap
|
||||||
|
|
||||||
|
signal combat_requested(attacker: Unit, defender: Unit)
|
||||||
|
|
||||||
var _selected_unit: Unit = null
|
var _selected_unit: Unit = null
|
||||||
var _target_pos: Vector2
|
var _target_pos: Vector2
|
||||||
var _goal_pos: Vector2
|
var _goal_pos: Vector2
|
||||||
@@ -16,7 +18,10 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
var clicked_unit := _get_unit_at(world_pos)
|
var clicked_unit := _get_unit_at(world_pos)
|
||||||
|
|
||||||
if clicked_unit:
|
if clicked_unit:
|
||||||
_select_unit(clicked_unit)
|
if _selected_unit and clicked_unit != _selected_unit:
|
||||||
|
combat_requested.emit(_selected_unit, clicked_unit)
|
||||||
|
else:
|
||||||
|
_select_unit(clicked_unit)
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
elif _selected_unit:
|
elif _selected_unit:
|
||||||
var snapped_pos := dl_map.snap_to_grid(world_pos)
|
var snapped_pos := dl_map.snap_to_grid(world_pos)
|
||||||
|
|||||||
13
resources/resource_definitions/combat_proposal.gd
Normal file
13
resources/resource_definitions/combat_proposal.gd
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class_name CombatProposal extends Resource
|
||||||
|
|
||||||
|
class CombatantStats:
|
||||||
|
var unit: Unit
|
||||||
|
var hp: int
|
||||||
|
var sp: int
|
||||||
|
var hit: int
|
||||||
|
var atk: int
|
||||||
|
var def: int
|
||||||
|
var spd: int
|
||||||
|
|
||||||
|
var attacker: CombatantStats
|
||||||
|
var defender: CombatantStats
|
||||||
1
resources/resource_definitions/combat_proposal.gd.uid
Normal file
1
resources/resource_definitions/combat_proposal.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b4oatflqabi37
|
||||||
@@ -2,8 +2,10 @@ class_name UnitStats extends Resource
|
|||||||
|
|
||||||
@export var max_hp: int = 10
|
@export var max_hp: int = 10
|
||||||
@export var current_hp: int
|
@export var current_hp: int
|
||||||
@export var phys_atk: int = 1
|
@export var max_sp: int = 10
|
||||||
@export var phys_def: int = 1
|
@export var current_sp: int = 10
|
||||||
|
@export var phys_atk: int = 10
|
||||||
|
@export var phys_def: int = 5
|
||||||
@export var magic_atk: int = 0
|
@export var magic_atk: int = 0
|
||||||
@export var magic_def: int = 0
|
@export var magic_def: int = 0
|
||||||
@export var hit: int = 85
|
@export var hit: int = 85
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
resource_name = "StartButton"
|
resource_name = "StartButton"
|
||||||
script/source = "extends Button
|
script/source = "extends Button
|
||||||
|
|
||||||
const COMBAT_SCENE = preload(\"res://scenes/combat_test.tscn\")
|
const COMBAT_SCENE = preload(\"res://scenes/strategy_phase.tscn\")
|
||||||
const UNIT_SCENE = preload(\"res://prefabs/unit.tscn\")
|
const UNIT_SCENE = preload(\"res://prefabs/unit.tscn\")
|
||||||
const PLAYER_ALLEGIANCE = preload(\"res://resources/allegiance_types/player_allegiance.tres\")
|
const PLAYER_ALLEGIANCE = preload(\"res://resources/allegiance_types/player_allegiance.tres\")
|
||||||
const ENEMY_ALLEGIANCE = preload(\"res://resources/allegiance_types/enemy_allegiance.tres\")
|
const ENEMY_ALLEGIANCE = preload(\"res://resources/allegiance_types/enemy_allegiance.tres\")
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
[gd_scene format=3 uid="uid://wy7ur5r23ek3"]
|
[gd_scene format=3 uid="uid://wy7ur5r23ek3"]
|
||||||
|
|
||||||
[ext_resource type="PackedScene" uid="uid://cy7r0udfcsqbn" path="res://prefabs/combat_ui.tscn" id="1_5jbmu"]
|
[ext_resource type="PackedScene" uid="uid://cy7r0udfcsqbn" path="res://prefabs/combat_ui.tscn" id="1_6gip4"]
|
||||||
[ext_resource type="PackedScene" uid="uid://dkhyh5ce4iuk3" path="res://prefabs/combat_map.tscn" id="1_7abyo"]
|
[ext_resource type="PackedScene" uid="uid://dkhyh5ce4iuk3" path="res://prefabs/combat_map.tscn" id="2_iuoca"]
|
||||||
[ext_resource type="Script" uid="uid://csdcbi2gtwrly" path="res://scripts/camera_controller.gd" id="3_cam"]
|
[ext_resource type="Script" uid="uid://dfojm3n0em4ef" path="res://nodes/player_controller.gd" id="3_esrqm"]
|
||||||
[ext_resource type="Script" uid="uid://dfojm3n0em4ef" path="res://nodes/player_controller.gd" id="4_s5ga2"]
|
[ext_resource type="Script" uid="uid://csdcbi2gtwrly" path="res://scripts/camera_controller.gd" id="4_ww3c6"]
|
||||||
[ext_resource type="AudioStream" uid="uid://dsikulned64qt" path="res://assets/music/combat_bgm_01.OGG" id="6_0yobm"]
|
[ext_resource type="AudioStream" uid="uid://dsikulned64qt" path="res://assets/music/combat_bgm_01.OGG" id="5_ficdm"]
|
||||||
|
[ext_resource type="Script" uid="uid://cf4ivrcbky0s3" path="res://nodes/combat_system.gd" id="6_combat"]
|
||||||
|
|
||||||
[node name="CombatTest" type="Node2D" unique_id=855645983]
|
[node name="CombatTest" type="Node2D" unique_id=855645983]
|
||||||
|
|
||||||
[node name="CombatUI" parent="." unique_id=329168107 instance=ExtResource("1_5jbmu")]
|
[node name="CombatUI" parent="." unique_id=329168107 instance=ExtResource("1_6gip4")]
|
||||||
|
|
||||||
[node name="CombatMap" parent="." unique_id=546780706 instance=ExtResource("1_7abyo")]
|
[node name="CombatMap" parent="." unique_id=546780706 instance=ExtResource("2_iuoca")]
|
||||||
|
|
||||||
[node name="PlayerController" type="Node" parent="." unique_id=774568109 node_paths=PackedStringArray("dl_map")]
|
[node name="PlayerController" type="Node" parent="." unique_id=774568109 node_paths=PackedStringArray("dl_map")]
|
||||||
script = ExtResource("4_s5ga2")
|
script = ExtResource("3_esrqm")
|
||||||
dl_map = NodePath("../CombatMap")
|
dl_map = NodePath("../CombatMap")
|
||||||
|
|
||||||
|
[node name="CombatSystem" type="Node" parent="." unique_id=1234567890]
|
||||||
|
script = ExtResource("6_combat")
|
||||||
|
|
||||||
[node name="Camera2D" type="Camera2D" parent="." unique_id=1739569732]
|
[node name="Camera2D" type="Camera2D" parent="." unique_id=1739569732]
|
||||||
zoom = Vector2(1.5, 1.5)
|
zoom = Vector2(1.5, 1.5)
|
||||||
script = ExtResource("3_cam")
|
script = ExtResource("4_ww3c6")
|
||||||
|
|
||||||
|
|
||||||
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="." unique_id=1057500234]
|
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="." unique_id=1057500234]
|
||||||
stream = ExtResource("6_0yobm")
|
stream = ExtResource("5_ficdm")
|
||||||
autoplay = true
|
autoplay = true
|
||||||
|
|
||||||
|
[connection signal="combat_requested" from="PlayerController" to="CombatSystem" method="process_combat"]
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
shader = ExtResource("1_nd71p")
|
shader = ExtResource("1_nd71p")
|
||||||
shader_parameter/flag_mask = ExtResource("2_7ddre")
|
shader_parameter/flag_mask = ExtResource("2_7ddre")
|
||||||
shader_parameter/team_color = Color(0.84830123, 0.29993045, 0.292207, 1)
|
shader_parameter/team_color = Color(0.84830123, 0.29993045, 0.292207, 1)
|
||||||
|
shader_parameter/chroma_threshold = 0.10000000475
|
||||||
|
|
||||||
[sub_resource type="AtlasTexture" id="AtlasTexture_j8ivh"]
|
[sub_resource type="AtlasTexture" id="AtlasTexture_j8ivh"]
|
||||||
atlas = ExtResource("1_g7g4h")
|
atlas = ExtResource("1_g7g4h")
|
||||||
|
|||||||
@@ -2,9 +2,18 @@ shader_type canvas_item;
|
|||||||
|
|
||||||
uniform sampler2D flag_mask : source_color, hint_default_black;
|
uniform sampler2D flag_mask : source_color, hint_default_black;
|
||||||
uniform vec4 team_color : source_color = vec4(1.0, 0.0, 0.0, 1.0); // red by default
|
uniform vec4 team_color : source_color = vec4(1.0, 0.0, 0.0, 1.0); // red by default
|
||||||
|
uniform float chroma_threshold : hint_range(0.0, 1.0) = 0.01;
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec4 base = texture(TEXTURE, UV);
|
vec4 base = texture(TEXTURE, UV);
|
||||||
|
|
||||||
|
// --- Chroma key: discard near-black pixels ---
|
||||||
|
float max_channel = max(base.r, max(base.g, base.b));
|
||||||
|
if (max_channel < chroma_threshold) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Palette swap using masks ---
|
||||||
float mask = texture(flag_mask, UV).r; // 1.0 on flag pixels, 0.0 on castle
|
float mask = texture(flag_mask, UV).r; // 1.0 on flag pixels, 0.0 on castle
|
||||||
|
|
||||||
// Use the grey luminance as a brightness multiplier on the team color
|
// Use the grey luminance as a brightness multiplier on the team color
|
||||||
|
|||||||
Reference in New Issue
Block a user