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:
|
||||
return Vector2(floorf(pos.x / TILE_SIZE), floorf(pos.y / TILE_SIZE)) * TILE_SIZE
|
||||
|
||||
|
||||
func world_to_coords(pos: Vector2) -> Vector2i:
|
||||
return Vector2i(snap_to_grid(pos) / TILE_SIZE)
|
||||
|
||||
|
||||
func coords_to_world(coords: Vector2i) -> Vector2:
|
||||
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
|
||||
|
||||
signal combat_requested(attacker: Unit, defender: Unit)
|
||||
|
||||
var _selected_unit: Unit = null
|
||||
var _target_pos: Vector2
|
||||
var _goal_pos: Vector2
|
||||
@@ -16,6 +18,9 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
var clicked_unit := _get_unit_at(world_pos)
|
||||
|
||||
if 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()
|
||||
elif _selected_unit:
|
||||
|
||||
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 current_hp: int
|
||||
@export var phys_atk: int = 1
|
||||
@export var phys_def: int = 1
|
||||
@export var max_sp: int = 10
|
||||
@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_def: int = 0
|
||||
@export var hit: int = 85
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
resource_name = "StartButton"
|
||||
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 PLAYER_ALLEGIANCE = preload(\"res://resources/allegiance_types/player_allegiance.tres\")
|
||||
const ENEMY_ALLEGIANCE = preload(\"res://resources/allegiance_types/enemy_allegiance.tres\")
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
[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://dkhyh5ce4iuk3" path="res://prefabs/combat_map.tscn" id="1_7abyo"]
|
||||
[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="4_s5ga2"]
|
||||
[ext_resource type="AudioStream" uid="uid://dsikulned64qt" path="res://assets/music/combat_bgm_01.OGG" id="6_0yobm"]
|
||||
[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="2_iuoca"]
|
||||
[ext_resource type="Script" uid="uid://dfojm3n0em4ef" path="res://nodes/player_controller.gd" id="3_esrqm"]
|
||||
[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="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="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")]
|
||||
script = ExtResource("4_s5ga2")
|
||||
script = ExtResource("3_esrqm")
|
||||
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]
|
||||
zoom = Vector2(1.5, 1.5)
|
||||
script = ExtResource("3_cam")
|
||||
|
||||
script = ExtResource("4_ww3c6")
|
||||
|
||||
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="." unique_id=1057500234]
|
||||
stream = ExtResource("6_0yobm")
|
||||
stream = ExtResource("5_ficdm")
|
||||
autoplay = true
|
||||
|
||||
[connection signal="combat_requested" from="PlayerController" to="CombatSystem" method="process_combat"]
|
||||
@@ -8,6 +8,7 @@
|
||||
shader = ExtResource("1_nd71p")
|
||||
shader_parameter/flag_mask = ExtResource("2_7ddre")
|
||||
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"]
|
||||
atlas = ExtResource("1_g7g4h")
|
||||
|
||||
@@ -2,9 +2,18 @@ shader_type canvas_item;
|
||||
|
||||
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 float chroma_threshold : hint_range(0.0, 1.0) = 0.01;
|
||||
|
||||
void fragment() {
|
||||
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
|
||||
|
||||
// Use the grey luminance as a brightness multiplier on the team color
|
||||
|
||||
Reference in New Issue
Block a user