Skip to content

Commit 115aa27

Browse files
committed
Tightened up static typing and added more combat examples.
1 parent b91f60b commit 115aa27

File tree

6 files changed

+95
-45
lines changed

6 files changed

+95
-45
lines changed

Projects/damage-engine/scripts/combatant.gd

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ const BACK := 1
1212
@export var armor: Equipment
1313

1414
# Defender’s per-element tags: { Element.Type.FIRE: "weak"/"resist"/"immune"/"absorb"/"normal" }
15-
@export var element_resist: Dictionary = {}
15+
@export var element_resist: Dictionary[Element.Type, String] = {}
1616

1717
# Active status flags
18-
var status: Dictionary = {}
18+
var status: Dictionary[StatusEffects.Status, bool] = {}
1919

2020
func _ready():
2121
if base_stats == null:
@@ -35,7 +35,7 @@ func total_stats() -> Stats:
3535
s.set_stat(k, s.get_stat(k) + int(armor.stat_bonus[k]))
3636
return s
3737

38-
func weapon_elements() -> Array:
38+
func weapon_elements() -> Array[Element.Type]:
3939
if weapon and weapon.elements.size() > 0:
4040
return Element.combine(weapon.elements)
4141
else:
@@ -53,5 +53,5 @@ func take_damage(n: int) -> void:
5353
func heal(n: int) -> void:
5454
base_stats.hp = min(base_stats.max_hp, base_stats.hp + n)
5555

56-
func set_status_flag(k: String, v: bool) -> void:
56+
func set_status_flag(k: StatusEffects.Status, v: bool) -> void:
5757
status[k] = v

Projects/damage-engine/scripts/damage_engine.gd

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,18 @@ const CRIT_AGI_SCALE := 0.0015 # +0.15% crit per AGI
3434
const CRIT_MULT := 1.5
3535
const VARIANCE_LOW := 0.93
3636
const VARIANCE_HIGH := 1.07
37-
const DAMAGE_CAP := 9999 # maximum non-absorbed damage dealt
37+
const DAMAGE_CAP := 9999
3838

3939
# Hit/Evasion (physical only)
40-
const HIT_BASE := 0.90 # base 90% hit chance
41-
const HIT_AGI_DIFF_SCALE := 0.003 # ±0.3% per AGI difference (attacker - defender)
42-
const HIT_MIN := 0.05 # never below 5%
43-
const HIT_MAX := 0.99 # never above 99%
40+
const HIT_BASE := 0.90
41+
const HIT_AGI_DIFF_SCALE := 0.003
42+
const HIT_MIN := 0.05
43+
const HIT_MAX := 0.99
4444
const BLIND_HIT_MULT := 0.5 # blind halves hit chance
4545

4646
# Rows (physical only)
47-
const BACKROW_OUT := 0.5 # attacker in back (non long-range) halves output
48-
const BACKROW_IN := 0.5 # defender in back halves incoming
47+
const BACKROW_OUT := 0.5
48+
const BACKROW_IN := 0.5
4949

5050
# ---------- Helpers ----------
5151
static func _rand_range(a: float, b: float) -> float:
@@ -54,7 +54,7 @@ static func _rand_range(a: float, b: float) -> float:
5454
static func _roll_crit(agi: int) -> bool:
5555
return randf() < (CRIT_BASE + float(agi) * CRIT_AGI_SCALE)
5656

57-
static func _element_mult(attacker_elems: Array, defender_resist: Dictionary) -> float:
57+
static func _element_mult(attacker_elems: Array[Element.Type], defender_resist: Dictionary[Element.Type, String]) -> float:
5858
return Element.vs_defender(attacker_elems, defender_resist)
5959

6060
static func _row_mult(attacker: Combatant, defender: Combatant) -> float:
@@ -69,7 +69,7 @@ static func _roll_phys_hit(attacker: Combatant, defender: Combatant) -> bool:
6969
var A := attacker.total_stats()
7070
var D := defender.total_stats()
7171
var chance := HIT_BASE + float(A.agi - D.agi) * HIT_AGI_DIFF_SCALE
72-
if StatusEffects.has(attacker.status, "blind"):
72+
if StatusEffects.has(attacker.status, StatusEffects.Status.BLIND):
7373
chance *= BLIND_HIT_MULT
7474
chance = clamp(chance, HIT_MIN, HIT_MAX)
7575
return randf() < chance

Projects/damage-engine/scripts/damage_test.gd

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ extends Node2D
44
@onready var hero: Combatant = $"../Hero"
55
@onready var foe: Combatant = $"../Enemy"
66

7+
func clear_status(u: Combatant) -> void:
8+
for k in StatusEffects.KEYS.keys():
9+
u.set_status_flag(k, false)
10+
11+
func demo_phys(label: String) -> void:
12+
var hp0 := foe.base_stats.hp
13+
var r := FF4DamageEngine.physical_damage(hero, foe)
14+
FF4DamageEngine.apply_result(foe, r)
15+
print(label, " -> ", r.pretty_str(), " (Enemy HP ", hp0, " -> ", foe.base_stats.hp, ")")
16+
foe.base_stats.hp = hp0
17+
18+
func demo_mag(label: String, power: int, elem: Element.Type) -> void:
19+
var hp0 := foe.base_stats.hp
20+
var r := FF4DamageEngine.magical_damage(hero, foe, power, elem)
21+
r.apply_to(foe)
22+
print(label, " -> ", r.pretty_str(), " (Enemy HP ", hp0, " -> ", foe.base_stats.hp, ")")
23+
foe.base_stats.hp = hp0
24+
725
func _ready():
826
randomize()
927
# Example enemy resistances
@@ -14,11 +32,44 @@ func _ready():
1432
Element.Type.DARK: "absorb"
1533
}
1634

17-
# Physical attack
18-
var r1 : FF4DamageEngine.CombatResult = FF4DamageEngine.physical_damage(hero, foe)
19-
FF4DamageEngine.apply_result(foe, r1)
20-
print("Phys -> ", r1.pretty_str())
35+
# Baseline (no statuses)
36+
clear_status(hero)
37+
clear_status(foe)
38+
demo_phys("Phys baseline")
39+
demo_mag("Fire baseline", 30, Element.Type.FIRE)
40+
41+
# Defender has Protect (physical incoming halved)
42+
clear_status(hero)
43+
clear_status(foe)
44+
foe.set_status_flag(StatusEffects.Status.PROTECT, true)
45+
demo_phys("Phys vs PROTECT")
46+
47+
# Defender is Defending (halves incoming physical)
48+
clear_status(hero)
49+
clear_status(foe)
50+
foe.set_status_flag(StatusEffects.Status.DEFENDING, true)
51+
demo_phys("Phys vs DEFENDING")
52+
53+
# Attacker is Berserk (physical output up)
54+
clear_status(hero)
55+
clear_status(foe)
56+
hero.set_status_flag(StatusEffects.Status.BERSERK, true)
57+
demo_phys("Phys with BERSERK")
58+
59+
# Attacker is Blind (physical output down and lower hit chance; may miss)
60+
clear_status(hero)
61+
clear_status(foe)
62+
hero.set_status_flag(StatusEffects.Status.BLIND, true)
63+
for i in 3:
64+
demo_phys("Phys with BLIND (try %d)" % (i + 1))
65+
66+
# Defender has Shell (magical incoming halved)
67+
clear_status(hero)
68+
clear_status(foe)
69+
foe.set_status_flag(StatusEffects.Status.SHELL, true)
70+
demo_mag("Fire vs SHELL", 30, Element.Type.FIRE)
2171

22-
var r2: FF4DamageEngine.CombatResult = FF4DamageEngine.magical_damage(hero, foe, 30, Element.Type.FIRE)
23-
r2.apply_to(foe) # instance method also available
24-
print("Fire -> ", r2.pretty_str())
72+
# Magical absorb example (Dark is absorbed per resist table above)
73+
clear_status(hero)
74+
clear_status(foe)
75+
demo_mag("Dark absorb", 30, Element.Type.DARK)

Projects/damage-engine/scripts/element.gd

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ const MULT := {
1717
"absorb": -1.0
1818
}
1919

20-
static func combine(elems: Array) -> Array:
21-
var out: Array = []
20+
static func combine(elems: Array[Element.Type]) -> Array[Element.Type]:
21+
var out: Array[Element.Type] = []
2222
for e in elems:
2323
if e != Type.NONE and not out.has(e):
2424
out.append(e)
2525
return out
2626

27-
static func vs_defender(attacker_elems: Array, defender_resist: Dictionary) -> float:
27+
static func vs_defender(attacker_elems: Array[Element.Type], defender_resist: Dictionary[Element.Type, String]) -> float:
2828
if attacker_elems.is_empty(): return 1.0
2929
var m := 1.0
3030
for e in attacker_elems:

Projects/damage-engine/scripts/equipment.gd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class_name Equipment
44
@export var name: String = "Equipment"
55
@export var long_range: bool = false # bows/whips ignore back-row penalty on attacker
66
@export var elements: Array[Element.Type] = [] # applied on physical only (typical JRPG convention)
7-
@export var stat_bonus: Dictionary = {
7+
@export var stat_bonus: Dictionary[Stats.Stat, int] = {
88
Stats.Stat.STR: 0,
99
Stats.Stat.VIT: 0,
1010
Stats.Stat.AGI: 0,
Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,35 @@
11
class_name StatusEffects
22
extends Resource
33

4-
5-
# Boolean flags
6-
const KEYS := {
7-
"defending": false, # Defend command (halves incoming)
8-
"protect": false, # Physical incoming halved
9-
"shell": false, # Magical incoming halved
10-
"blind": false, # We’ll model as physical output penalty
11-
"berserk": false, # Physical output bonus
12-
"poison": false
4+
# Enumerated status flags
5+
enum Status { DEFENDING, PROTECT, SHELL, BLIND, BERSERK, POISON }
6+
7+
# Default boolean flags keyed by Status enum
8+
const KEYS: Dictionary[Status, bool] = {
9+
Status.DEFENDING: false, # Defend command (halves incoming)
10+
Status.PROTECT: false, # Physical incoming halved
11+
Status.SHELL: false, # Magical incoming halved
12+
Status.BLIND: false, # Physical output penalty, also affects hit chance
13+
Status.BERSERK: false, # Physical output bonus
14+
Status.POISON: false
1315
}
1416

15-
static func has(s: Dictionary, k: String) -> bool:
17+
static func has(s: Dictionary[Status, bool], k: Status) -> bool:
1618
return bool(s.get(k, false))
1719

18-
19-
static func physical_out_mult(s: Dictionary) -> float:
20+
static func physical_out_mult(s: Dictionary[Status, bool]) -> float:
2021
var m := 1.0
21-
if has(s, "berserk"): m *= 1.5
22-
if has(s, "blind"): m *= 0.75
22+
if has(s, Status.BERSERK): m *= 1.5
23+
if has(s, Status.BLIND): m *= 0.75
2324
return m
2425

25-
26-
static func physical_in_mult(s: Dictionary) -> float:
26+
static func physical_in_mult(s: Dictionary[Status, bool]) -> float:
2727
var m := 1.0
28-
if has(s, "protect"): m *= 0.5
29-
if has(s, "defending"): m *= 0.5
28+
if has(s, Status.PROTECT): m *= 0.5
29+
if has(s, Status.DEFENDING): m *= 0.5
3030
return m
3131

32-
33-
static func magical_in_mult(s: Dictionary) -> float:
32+
static func magical_in_mult(s: Dictionary[Status, bool]) -> float:
3433
var m := 1.0
35-
if has(s, "shell"): m *= 0.5
34+
if has(s, Status.SHELL): m *= 0.5
3635
return m

0 commit comments

Comments
 (0)