Reorganize the codebase into role-based folders (snake_case)
Move the 40-odd root-level scripts and scenes into a tree that groups by role,
per the approved roadmap (WS2/WS3): actors/{player,matador,bear}, systems/{combat,
fx,input,debug,render,crowd,config}, ui/, props/ (was tscn_s/). Rename the shell
scene.tscn -> levels/game_shell.tscn and every PascalCase scene to snake_case
(Player.tscn -> player.tscn, MainMenu.tscn -> main_menu.tscn, etc.) so the
case-insensitive web/macOS filesystems can't alias two files together.
Done as a scripted move: each file travels with its .uid; every quoted res:// path
across scenes, scripts, .tres, and project.godot's 9 autoloads is rewritten; the
.godot uid/class cache is rebuilt from scratch. Scenes still resolve by uid, so the
rewrite is belt-and-suspenders. Lint globs (run_tests.sh + both CI workflows) now
walk the new dirs; an orphan animation_tree.gd.uid and 7 pre-existing trailing-
whitespace lints (barrel/torch, never covered by the old root-only glob) are cleaned.
Full suite green on both sides. Asset consolidation (Assets/Animations/fonts ->
art/) is deferred — moving imported .glb/.png/.ttf is safer done in the editor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017KoYBYQgRyY1zDHXMsXY3V
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
class_name DamageEvent
|
||||
extends RefCounted
|
||||
## The one typed payload for every hit in the game — a bull goring a matador, a bear slam popping
|
||||
## the bull, a thrown spear landing. It replaces the scattered, stringly Object.call(&"take_sword_hit",
|
||||
## "gored") paths: an emitter (a Hitbox) fills one of these and hands it to the target's
|
||||
## receive_damage(event). One shape to read, log and type-check, instead of four call conventions.
|
||||
##
|
||||
## It serves both victim models the game already has: the bull, which dies on one clean hit
|
||||
## (lethal + cause + hit_position), and an enemy, which is launched/ragdolled by an impulse
|
||||
## (knockback_dir · knockback_strength + knock_up). A given hit just leaves the fields it doesn't
|
||||
## use at their defaults.
|
||||
|
||||
enum Cause { GENERIC, GORED, MAULED, THROWN, SLAMMED, KICKED, DASHED }
|
||||
|
||||
## Who dealt the hit — for attribution and for ignoring self-overlap. May be null.
|
||||
var source: Node = null
|
||||
## What kind of hit; drives the victim's reaction (blood/vignette/FX) and any future resistances.
|
||||
var cause: Cause = Cause.GENERIC
|
||||
## World contact point, so impact FX spawn where the hit actually landed.
|
||||
var hit_position: Vector3 = Vector3.ZERO
|
||||
## Planar push direction (need not be normalised). Vector3.ZERO = no horizontal knockback.
|
||||
var knockback_dir: Vector3 = Vector3.ZERO
|
||||
## Impulse magnitude behind knockback_dir — the enemy launch/ragdoll strength.
|
||||
var knockback_strength: float = 0.0
|
||||
## Upward launch in m/s (an overhead smash pop, a leap-slam outer ring). 0 = a grounded hit.
|
||||
var knock_up: float = 0.0
|
||||
## One clean hit ends the run for the bull; most enemy-dealt hits are lethal to it. Enemies that
|
||||
## are merely launched (not killed) by the bull's abilities set this false.
|
||||
var lethal: bool = true
|
||||
|
||||
|
||||
static func make(kind: Cause, at: Vector3, src: Node = null) -> DamageEvent:
|
||||
var e := DamageEvent.new()
|
||||
e.cause = kind
|
||||
e.hit_position = at
|
||||
e.source = src
|
||||
return e
|
||||
|
||||
|
||||
## The single entry point every attacker uses to wound a target: build the typed event and hand it
|
||||
## to receive_damage(). Falls back to the legacy string call for any target not yet migrated, so a
|
||||
## caller never has to branch on which contract the victim speaks. `knock_up_v` is the already-shaped
|
||||
## pop (0 = grounded); callers that scale it by distance compute it before calling.
|
||||
static func deal(target: Node, kind: Cause, at: Vector3, src: Node = null, knock_up_v: float = 0.0) -> void:
|
||||
if target == null:
|
||||
return
|
||||
if target.has_method(&"receive_damage"):
|
||||
var event := make(kind, at, src)
|
||||
event.knock_up = knock_up_v
|
||||
target.call(&"receive_damage", event)
|
||||
return
|
||||
if target.has_method(&"take_sword_hit"):
|
||||
target.call(&"take_sword_hit", make(kind, at, src).cause_name(), at)
|
||||
if knock_up_v > 0.0 and target.has_method(&"apply_knock_up"):
|
||||
target.call(&"apply_knock_up", knock_up_v)
|
||||
|
||||
|
||||
## Lower-case name of the cause, for logs and for the bull's existing cause-keyed reactions
|
||||
## ("gored" / "mauled" / "thrown") during the migration off string causes.
|
||||
func cause_name() -> String:
|
||||
return String(Cause.keys()[cause]).to_lower()
|
||||
|
||||
|
||||
## Maps a legacy cause string onto the enum, so a Hitbox's exported damage_cause round-trips
|
||||
## exactly (gored/mauled/thrown are the only ones in use). Unknown strings fall back to GENERIC.
|
||||
static func cause_from(text: String) -> Cause:
|
||||
match text.to_lower():
|
||||
"gored":
|
||||
return Cause.GORED
|
||||
"mauled":
|
||||
return Cause.MAULED
|
||||
"thrown":
|
||||
return Cause.THROWN
|
||||
"slammed":
|
||||
return Cause.SLAMMED
|
||||
"kicked":
|
||||
return Cause.KICKED
|
||||
"dashed":
|
||||
return Cause.DASHED
|
||||
return Cause.GENERIC
|
||||
@@ -0,0 +1 @@
|
||||
uid://cwnr3ggsrii76
|
||||
@@ -0,0 +1,98 @@
|
||||
@tool
|
||||
extends Area3D
|
||||
class_name Hitbox
|
||||
## A designer-placeable attack hitbox: drop this into a scene, give it a CollisionShape3D
|
||||
## child, and drag the shape to size/position the attack's reach in the editor. The shape
|
||||
## itself decides what got hit — no distance/facing math in code.
|
||||
##
|
||||
## Inactive by default; the owning script calls activate() for the swing's active window and
|
||||
## deactivate() when it ends. Each activation only hits a given body once (the bull's own
|
||||
## i-frames also collapse a flurry, but this keeps a single swing from re-triggering).
|
||||
|
||||
const PhysicsLayers = preload("res://systems/combat/physics_layers.gd")
|
||||
|
||||
## How the hit is tagged on the bull's take_sword_hit ("mauled" / "gored" / …).
|
||||
@export var damage_cause: String = "mauled"
|
||||
## Upward launch (m/s) applied to the bull on hit — e.g. an overhead smash that pops it up.
|
||||
@export var knock_up: float = 0.0
|
||||
## Knock-up distance shaping, measured planar (XZ) from this hitbox's own origin to the body:
|
||||
## below `knock_up_min_dist` no pop is applied (leap slam: only the outer ring launches). If
|
||||
## `knock_up_max_dist` > min, the pop ramps 0→`knock_up` from min to max (overhead smash: the
|
||||
## further out the hit lands, the higher the bull flies); at/again 0 it's full past the min.
|
||||
@export var knock_up_min_dist: float = 0.0
|
||||
@export var knock_up_max_dist: float = 0.0
|
||||
|
||||
var _active: bool = false
|
||||
var _hit_this_swing: Dictionary = {}
|
||||
# Whether the most recent active window connected. Cleared on activate(), NOT deactivate(), so the
|
||||
# owner can read it after the window closes (e.g. a landed stab breaks off to taunt).
|
||||
var _did_hit: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Detect the bull's body (layer BULL) only; never collide or push physics.
|
||||
collision_layer = 0
|
||||
collision_mask = PhysicsLayers.BULL
|
||||
monitorable = false
|
||||
monitoring = false
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
body_entered.connect(_on_body_entered)
|
||||
|
||||
|
||||
func activate() -> void:
|
||||
if _active:
|
||||
return
|
||||
_active = true
|
||||
_hit_this_swing.clear()
|
||||
_did_hit = false
|
||||
monitoring = true
|
||||
|
||||
|
||||
func deactivate() -> void:
|
||||
_active = false
|
||||
monitoring = false
|
||||
|
||||
|
||||
# Did the window that just closed connect? Valid until the next activate() clears it.
|
||||
func did_hit() -> bool:
|
||||
return _did_hit
|
||||
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
_apply_hit(body)
|
||||
|
||||
|
||||
# For AoE slams the bull is usually already standing inside the shape when it activates, so
|
||||
# body_entered never fires. The owner calls sweep() each active frame to catch current overlaps
|
||||
# (still deduped per activation, so the bull is only hit once per slam).
|
||||
func sweep() -> void:
|
||||
if not _active:
|
||||
return
|
||||
for body in get_overlapping_bodies():
|
||||
_apply_hit(body)
|
||||
|
||||
|
||||
func _apply_hit(body: Node3D) -> void:
|
||||
if not _active or not body.is_in_group(&"player"):
|
||||
return
|
||||
if _hit_this_swing.has(body):
|
||||
return
|
||||
_hit_this_swing[body] = true
|
||||
_did_hit = true
|
||||
var ku := _knock_up_for(body) if knock_up > 0.0 else 0.0
|
||||
DamageEvent.deal(body, DamageEvent.cause_from(damage_cause), body.global_position, self, ku)
|
||||
|
||||
|
||||
# Scale the pop by how far the body is from this hitbox's origin (planar). See the export docs.
|
||||
func _knock_up_for(body: Node3D) -> float:
|
||||
if knock_up_min_dist <= 0.0 and knock_up_max_dist <= 0.0:
|
||||
return knock_up
|
||||
var to := body.global_position - global_position
|
||||
var d := Vector2(to.x, to.z).length()
|
||||
if d < knock_up_min_dist:
|
||||
return 0.0
|
||||
if knock_up_max_dist > knock_up_min_dist:
|
||||
var t := clampf((d - knock_up_min_dist) / (knock_up_max_dist - knock_up_min_dist), 0.0, 1.0)
|
||||
return knock_up * t
|
||||
return knock_up
|
||||
@@ -0,0 +1 @@
|
||||
uid://dlwjiw42q6b5f
|
||||
@@ -0,0 +1,11 @@
|
||||
## Central registry of the 3D physics collision-layer bits. These mirror the
|
||||
## layer_names in project.godot so the editor's layer picker reads the same words.
|
||||
##
|
||||
## The bull only collides with WORLD, so a killed matador is lifted off WORLD onto
|
||||
## CORPSE alone (see matador_ragdoll.gd). The ground and walls carry WORLD | CORPSE
|
||||
## (tagged at level load in game_shell.gd), so the ragdoll still rests on the arena
|
||||
## while the bull passes through the bodies instead of tripping over them.
|
||||
|
||||
const WORLD: int = 1 # layer 1 — environment + live actors (bull, matadors, spears)
|
||||
const BULL: int = 2 # layer 2 — the bull's own body
|
||||
const CORPSE: int = 4 # layer 3 — ragdolled matadors; ground/walls also carry this
|
||||
@@ -0,0 +1 @@
|
||||
uid://d01o2ul83waw5
|
||||
Reference in New Issue
Block a user