Skip to content

Commit 77792a6

Browse files
committed
Added first cut of a damage engine project.
1 parent d41eecd commit 77792a6

21 files changed

+478
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Normalize EOL for all files that Git considers text files.
2+
* text=auto eol=lf

Projects/damage-engine/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Godot 4+ specific ignores
2+
.godot/
3+
/android/

Projects/damage-engine/icon.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
[remap]
2+
3+
importer="texture"
4+
type="CompressedTexture2D"
5+
uid="uid://0wv8ltbjt58h"
6+
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
7+
metadata={
8+
"vram_texture": false
9+
}
10+
11+
[deps]
12+
13+
source_file="res://icon.svg"
14+
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
15+
16+
[params]
17+
18+
compress/mode=0
19+
compress/high_quality=false
20+
compress/lossy_quality=0.7
21+
compress/uastc_level=0
22+
compress/rdo_quality_loss=0.0
23+
compress/hdr_compression=1
24+
compress/normal_map=0
25+
compress/channel_pack=0
26+
mipmaps/generate=false
27+
mipmaps/limit=-1
28+
roughness/mode=0
29+
roughness/src_normal=""
30+
process/channel_remap/red=0
31+
process/channel_remap/green=1
32+
process/channel_remap/blue=2
33+
process/channel_remap/alpha=3
34+
process/fix_alpha_border=true
35+
process/premult_alpha=false
36+
process/normal_map_invert_y=false
37+
process/hdr_as_srgb=false
38+
process/hdr_clamp_exposure=false
39+
process/size_limit=0
40+
detect_3d/compress_to=1
41+
svg/scale=1.0
42+
editor/scale_with_editor_scale=false
43+
editor/convert_colors_with_editor_theme=false
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
; Engine configuration file.
2+
; It's best edited using the editor UI and not directly,
3+
; since the parameters that go here are not all obvious.
4+
;
5+
; Format:
6+
; [section] ; section goes between []
7+
; param=value ; assign values to parameters
8+
9+
config_version=5
10+
11+
[application]
12+
13+
config/name="DamageEngine"
14+
run/main_scene="uid://dq6qe51f47vw"
15+
config/features=PackedStringArray("4.5", "Mobile")
16+
config/icon="res://icon.svg"
17+
18+
[rendering]
19+
20+
renderer/rendering_method="mobile"
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
class_name Combatant
2+
extends Node
3+
4+
const FRONT := 0
5+
const BACK := 1
6+
7+
@export var display_name := "Unit"
8+
@export var row := FRONT
9+
10+
@export var base_stats: Stats
11+
@export var weapon: Equipment
12+
@export var armor: Equipment
13+
14+
# Defender’s per-element tags: { Element.Type.FIRE: "weak"/"resist"/"immune"/"absorb"/"normal" }
15+
@export var element_resist: Dictionary = {}
16+
17+
# Active status flags
18+
var status: Dictionary = {}
19+
20+
func _ready():
21+
if base_stats == null:
22+
base_stats = Stats.new()
23+
for k in StatusEffects.KEYS.keys():
24+
status[k] = StatusEffects.KEYS[k]
25+
26+
func total_stats() -> Stats:
27+
var s := base_stats.copy()
28+
if weapon:
29+
for k in weapon.stat_bonus.keys():
30+
var key = (k == "def") if "def" else k
31+
s.set_stat(key, s.get_stat(key) + int(weapon.stat_bonus[k]))
32+
if armor:
33+
for k in armor.stat_bonus.keys():
34+
var key = (k == "def") if "def" else k
35+
s.set_stat(key, s.get_stat(key) + int(armor.stat_bonus[k]))
36+
return s
37+
38+
func weapon_elements() -> Array:
39+
return Element.combine(weapon.elements) if weapon and weapon.elements.size() > 0 else []
40+
41+
func is_long_range_weapon() -> bool:
42+
return weapon != null and weapon.long_range
43+
44+
func is_alive() -> bool:
45+
return base_stats.hp > 0
46+
47+
func take_damage(n: int) -> void:
48+
base_stats.hp = max(0, base_stats.hp - n)
49+
50+
func heal(n: int) -> void:
51+
base_stats.hp = min(base_stats.max_hp, base_stats.hp + n)
52+
53+
func set_status_flag(k: String, v: bool) -> void:
54+
status[k] = v
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://ca7gd72aggdqc
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# res://scripts/DamageEngine.gd
2+
extends Node
3+
class_name FF4DamageEngine
4+
5+
# ---------- Inner result type ----------
6+
class CombatResult:
7+
var amount: int
8+
var crit: bool
9+
var absorbed: bool
10+
var elements: Array # Array[Element.Type]
11+
12+
func _init(amount: int = 0, crit: bool = false, absorbed: bool = false, elements: Array = []):
13+
self.amount = amount
14+
self.crit = crit
15+
self.absorbed = absorbed
16+
self.elements = elements.duplicate()
17+
18+
func apply_to(defender: Combatant) -> void:
19+
if absorbed:
20+
defender.heal(abs(amount))
21+
else:
22+
defender.take_damage(max(1, amount))
23+
24+
func pretty_str() -> String:
25+
return "{amount:%d, crit:%s, absorbed:%s, elements:%s}" % [amount, str(crit), str(absorbed), str(elements)]
26+
27+
# ---------- Tunables ----------
28+
const CRIT_BASE := 0.03 # base crit chance (no LUK)
29+
const CRIT_AGI_SCALE := 0.0015 # +0.15% crit per AGI
30+
const CRIT_MULT := 1.5
31+
const VARIANCE_LOW := 0.93
32+
const VARIANCE_HIGH := 1.07
33+
34+
# Rows (physical only)
35+
const BACKROW_OUT := 0.5 # attacker in back (non long-range) halves output
36+
const BACKROW_IN := 0.5 # defender in back halves incoming
37+
38+
# ---------- Helpers ----------
39+
static func _rand_range(a: float, b: float) -> float:
40+
return a + randf() * (b - a)
41+
42+
static func _roll_crit(agi: int) -> bool:
43+
return randf() < (CRIT_BASE + float(agi) * CRIT_AGI_SCALE)
44+
45+
static func _element_mult(attacker_elems: Array, defender_resist: Dictionary) -> float:
46+
return Element.vs_defender(attacker_elems, defender_resist)
47+
48+
static func _row_mult(attacker: Combatant, defender: Combatant) -> float:
49+
var m := 1.0
50+
if attacker.row == Combatant.BACK and not attacker.is_long_range_weapon():
51+
m *= BACKROW_OUT
52+
if defender.row == Combatant.BACK:
53+
m *= BACKROW_IN
54+
return m
55+
56+
# ---------- Physical damage ----------
57+
static func physical_damage(attacker: Combatant, defender: Combatant) -> CombatResult:
58+
var A := attacker.total_stats()
59+
var D := defender.total_stats()
60+
61+
var atk_term := A.atk + int(floor(A.strn / 2.0))
62+
var def_term := D.def_ + int(floor(D.vit / 2.0))
63+
64+
var crit := _roll_crit(A.agi)
65+
var def_factor : float = crit if 0.25 else 0.50
66+
var base : int = max(1, atk_term - int(floor(def_term * def_factor)))
67+
68+
base = int(round(base * _rand_range(VARIANCE_LOW, VARIANCE_HIGH)))
69+
70+
if crit:
71+
base = int(round(base * CRIT_MULT))
72+
73+
base = int(round(base * StatusEffects.physical_out_mult(attacker.status)))
74+
base = int(round(base * StatusEffects.physical_in_mult(defender.status)))
75+
76+
base = int(round(base * _row_mult(attacker, defender)))
77+
78+
var elems := attacker.weapon_elements()
79+
var em := _element_mult(elems, defender.element_resist)
80+
base = int(round(base * em))
81+
var absorbed := (em < 0.0)
82+
83+
if not absorbed:
84+
base = max(1, base)
85+
86+
return CombatResult.new(base, crit, absorbed, elems)
87+
88+
# ---------- Magical damage (single-element) ----------
89+
# Uses WIS for offense, WIL for resistance.
90+
static func magical_damage(attacker: Combatant, defender: Combatant, power: int, elem: Element.Type) -> CombatResult:
91+
var A := attacker.total_stats()
92+
var D := defender.total_stats()
93+
94+
var mag_term := power + int(floor(A.wis / 2.0)) + int(floor(A.mag / 2.0))
95+
var mres := int(floor(D.mdef / 2.0) + floor(D.wil / 2.0))
96+
97+
var base : int = max(1, mag_term - mres)
98+
base = int(round(base * _rand_range(VARIANCE_LOW, VARIANCE_HIGH)))
99+
base = int(round(base * StatusEffects.magical_in_mult(defender.status)))
100+
101+
var em := _element_mult([elem], defender.element_resist)
102+
base = int(round(base * em))
103+
var absorbed := (em < 0.0)
104+
105+
if not absorbed:
106+
base = max(1, base)
107+
108+
return CombatResult.new(base, false, absorbed, [elem])
109+
110+
# ---------- Optional: keep a static applier for convenience ----------
111+
static func apply_result(defender: Combatant, result: CombatResult) -> void:
112+
result.apply_to(defender)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uid://cl4heftt1h7ia

0 commit comments

Comments
 (0)