reskin wolf as hyena — visual + lean-in flavor
Replaces the procedural brown-rectangle "wolf" with the CraftPix Free Desert Enemy Sprite Sheets hyena. 48×48, side-view, 2-direction via horizontal flip. AnimatedSprite2D mounted in Wolf.setup() and Wolf.from_dict() (same pattern as pawn reskin Slice 1). Anims: idle (4f @ 5fps) + walk (6f @ 10fps). Attack/hurt/death anims skipped for MVP scope. Player-facing copy renamed wolf→hyena in strings.gd (6 entries) and event_catalog.gd (3 EventDef title/body fields). Internal identifiers (class Wolf, World.wolves, EventBus.wolf_spawned, save class_id &"wolf", event IDs like &"lone_wolf") stay the same for save compat — see header comment in wolf.gd. MCP runtime verified: hyena AnimatedSprite2D mounted on spawn, idle anim plays, storyteller modal renders "Lone Hyena — A starving hyena circles your livestock." Sprites: CraftPix Free Desert Enemy Sprite Sheets. License: CraftPix Free (commercial OK, attribution appreciated). https://free-game-assets.itch.io/free-enemy-sprite-sheets-pixel-art Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8a7584e411
commit
f30c7a858b
9 changed files with 179 additions and 31 deletions
|
|
@ -1 +0,0 @@
|
|||
uid://dyacrro784lvo
|
||||
34
scenes/entities/hyena_sprite_frames.gd
Normal file
34
scenes/entities/hyena_sprite_frames.gd
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
class_name HyenaSpriteFrames extends RefCounted
|
||||
## Builds a SpriteFrames resource for the Hyena (player-facing "hyena") entity.
|
||||
##
|
||||
## Atlas layout (single horizontal strip, 48×48 cells):
|
||||
## Hyena_idle.png — 192×48 = 4 frames
|
||||
## Hyena_walk.png — 288×48 = 6 frames
|
||||
##
|
||||
## Sprites: CraftPix Free Desert Enemy Sprite Sheets.
|
||||
## License: CraftPix Free (commercial OK, attribution appreciated).
|
||||
## https://free-game-assets.itch.io/free-enemy-sprite-sheets-pixel-art
|
||||
|
||||
const FRAME_SIZE: int = 48
|
||||
|
||||
## Build and return a SpriteFrames with "idle" (4f, 5 fps) and "walk" (6f, 10 fps).
|
||||
static func build() -> SpriteFrames:
|
||||
var sf := SpriteFrames.new()
|
||||
var idle_tex: Texture2D = preload("res://art/sprites/Hyena_idle.png")
|
||||
var walk_tex: Texture2D = preload("res://art/sprites/Hyena_walk.png")
|
||||
if sf.has_animation(&"default"):
|
||||
sf.remove_animation(&"default")
|
||||
_add_anim(sf, &"idle", idle_tex, 4, 5.0)
|
||||
_add_anim(sf, &"walk", walk_tex, 6, 10.0)
|
||||
return sf
|
||||
|
||||
|
||||
static func _add_anim(sf: SpriteFrames, anim_name: StringName, tex: Texture2D, count: int, fps: float) -> void:
|
||||
sf.add_animation(anim_name)
|
||||
sf.set_animation_loop(anim_name, true)
|
||||
sf.set_animation_speed(anim_name, fps)
|
||||
for i in count:
|
||||
var at := AtlasTexture.new()
|
||||
at.atlas = tex
|
||||
at.region = Rect2(i * FRAME_SIZE, 0, FRAME_SIZE, FRAME_SIZE)
|
||||
sf.add_frame(anim_name, at)
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
class_name Wolf extends Node2D
|
||||
## Wolf entity — hostile animal with a 4-state AI state machine.
|
||||
##
|
||||
## Player-facing copy renames this entity to "hyena" (see strings.gd, event_catalog.gd
|
||||
## labels). Internal class + registry identifiers remain "wolf"/"Wolf" for save
|
||||
## compatibility — class_id: &"wolf" in saves, World.wolves registry, EventBus.wolf_spawned.
|
||||
##
|
||||
## State machine (docs/architecture.md "Wolf AI"):
|
||||
## APPROACH → walk toward nearest non-downed pawn within sight radius.
|
||||
## ENGAGE → attack adjacent pawn; 70% hit chance; 50% chance to apply Bleeding.
|
||||
|
|
@ -21,6 +25,8 @@ const TILE_SIZE_PX: int = 16
|
|||
## Wolves move slightly faster than pawns (pawn STEP_TICKS = 10).
|
||||
const STEP_TICKS: int = 8
|
||||
|
||||
const _HYENA_SPRITE_FRAMES = preload("res://scenes/entities/hyena_sprite_frames.gd")
|
||||
|
||||
# ── combat tunables (Phase 10 placeholders; tune Phase 20) ──────────────────
|
||||
const ATTACK_DAMAGE: float = 12.0
|
||||
## 1.5 in-game seconds between attacks at 1× (30 ticks × 50 ms).
|
||||
|
|
@ -43,6 +49,7 @@ var target_pawn = null
|
|||
var _path: Array[Vector2i] = []
|
||||
var _step_progress: float = 0.0
|
||||
var _attack_cooldown: int = 0
|
||||
var _sprite: AnimatedSprite2D = null
|
||||
|
||||
## Transient: set by from_dict() to the saved target's pawn_name string.
|
||||
## SaveSystem._post_load_resolve_references() walks World.pawns, matches by
|
||||
|
|
@ -58,7 +65,6 @@ func _ready() -> void:
|
|||
position = _tile_to_world(tile)
|
||||
World.register_wolf(self)
|
||||
EventBus.sim_tick.connect(_on_sim_tick)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
|
|
@ -69,7 +75,7 @@ func _exit_tree() -> void:
|
|||
func setup(p_tile: Vector2i) -> void:
|
||||
tile = p_tile
|
||||
position = _tile_to_world(tile)
|
||||
queue_redraw()
|
||||
_mount_sprite()
|
||||
Audit.log("wolf", "spawned at %s" % tile)
|
||||
|
||||
|
||||
|
|
@ -78,6 +84,9 @@ func take_damage(amount: float) -> void:
|
|||
if hp <= 0.0 and state != State.DEAD:
|
||||
state = State.DEAD
|
||||
Audit.log("wolf", "wolf at %s killed" % tile)
|
||||
# Hide sprite; dead X marker is drawn by _draw() on the Node2D.
|
||||
if _sprite != null:
|
||||
_sprite.visible = false
|
||||
queue_redraw()
|
||||
|
||||
|
||||
|
|
@ -85,6 +94,26 @@ func is_dead() -> bool:
|
|||
return state == State.DEAD
|
||||
|
||||
|
||||
## Creates (or re-creates) the AnimatedSprite2D child. Idempotent — frees any
|
||||
## prior sprite so setup() and from_dict() can both call it safely.
|
||||
func _mount_sprite() -> void:
|
||||
if _sprite != null:
|
||||
_sprite.queue_free()
|
||||
_sprite = null
|
||||
_sprite = AnimatedSprite2D.new()
|
||||
_sprite.sprite_frames = _HYENA_SPRITE_FRAMES.build()
|
||||
# 48×48 sprite centred on the tile origin: shift up so the feet land at the
|
||||
# tile bottom edge (tile bottom = +8 px from centre → feet need to be at +8
|
||||
# → sprite centre at +8 − 24 = −16 px).
|
||||
_sprite.offset = Vector2(0.0, -16.0)
|
||||
# Y-sort: sit on the same plane as pawns (z_index 0, use y_sort_enabled on
|
||||
# the parent World node). Slightly behind pawns so they occlude the hyena
|
||||
# when occupying the same tile.
|
||||
_sprite.z_index = -1
|
||||
_sprite.play(&"idle")
|
||||
add_child(_sprite)
|
||||
|
||||
|
||||
# ── state machine tick ──────────────────────────────────────────────────────
|
||||
|
||||
func _on_sim_tick(_n: int) -> void:
|
||||
|
|
@ -224,7 +253,11 @@ func from_dict(d: Dictionary) -> void:
|
|||
if entry is Array and entry.size() == 2:
|
||||
_path.append(Vector2i(int(entry[0]), int(entry[1])))
|
||||
position = _tile_to_world(tile)
|
||||
queue_redraw()
|
||||
_mount_sprite()
|
||||
# If dead on load, hide sprite immediately (X marker drawn by _draw()).
|
||||
if state == State.DEAD and _sprite != null:
|
||||
_sprite.visible = false
|
||||
queue_redraw()
|
||||
|
||||
|
||||
# ── render ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -238,6 +271,22 @@ func _process(_delta: float) -> void:
|
|||
var to_w := _tile_to_world(to_t)
|
||||
position = from_w.lerp(to_w, _step_progress)
|
||||
|
||||
# Anim switching — avoid restarting the same anim each frame.
|
||||
if _sprite == null:
|
||||
return
|
||||
var moving: bool = not _path.is_empty()
|
||||
var target_anim := &"walk" if moving else &"idle"
|
||||
if _sprite.animation != target_anim:
|
||||
_sprite.play(target_anim)
|
||||
|
||||
# Facing — 2-direction horizontal flip; preserve last facing on pure vertical.
|
||||
if not _path.is_empty():
|
||||
var delta_x: int = _path[0].x - tile.x
|
||||
if delta_x > 0:
|
||||
_sprite.flip_h = false # moving right → face right (default)
|
||||
elif delta_x < 0:
|
||||
_sprite.flip_h = true # moving left → face left
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if state == State.DEAD:
|
||||
|
|
@ -245,20 +294,6 @@ func _draw() -> void:
|
|||
var x_color := Color(0.30, 0.10, 0.10, 0.8)
|
||||
draw_line(Vector2(-6, -6), Vector2(6, 6), x_color, 2.0)
|
||||
draw_line(Vector2(6, -6), Vector2(-6, 6), x_color, 2.0)
|
||||
return
|
||||
|
||||
# Dark-brown canine body (12×6 rect) plus a slightly lighter snout pellet.
|
||||
var body_col := Color(0.25, 0.22, 0.20, 1.0)
|
||||
var snout_col := Color(0.18, 0.15, 0.13, 1.0)
|
||||
# Body — horizontally elongated, centered slightly left of tile center.
|
||||
draw_rect(Rect2(Vector2(-7.0, -2.0), Vector2(12.0, 6.0)), body_col)
|
||||
# Snout — small block protruding to the right.
|
||||
draw_rect(Rect2(Vector2(4.0, -3.0), Vector2(5.0, 4.0)), snout_col)
|
||||
# Eye glow — single red dot; signals hostility at a glance.
|
||||
draw_circle(Vector2(6.0, -1.5), 0.7, Color(0.95, 0.30, 0.20, 0.95))
|
||||
# Legs — 4 short downward marks below the body.
|
||||
for x_off in [-5.0, -1.0, 2.0, 5.0]:
|
||||
draw_rect(Rect2(Vector2(x_off, 4.0), Vector2(1.5, 3.0)), body_col)
|
||||
|
||||
|
||||
# ── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ static func _event_04_walls() -> EventDef:
|
|||
var d := EventDef.new()
|
||||
d.id = &"walls"
|
||||
d.title = "Walls?"
|
||||
d.body = "Sleeping under stars is romantic until the wolves arrive."
|
||||
d.body = "Sleeping under stars is romantic until the hyenas arrive."
|
||||
d.category = EventDef.Category.NUDGE
|
||||
d.display = EventDef.Display.BANNER
|
||||
d.cooldown_days = 4
|
||||
|
|
@ -317,8 +317,8 @@ static func _event_12_the_wandering_healer() -> EventDef:
|
|||
static func _event_13_wolves_at_the_edge() -> EventDef:
|
||||
var d := EventDef.new()
|
||||
d.id = &"wolves_at_the_edge"
|
||||
d.title = "Wolves at the Edge"
|
||||
d.body = "Wolves howl in the distance. They will be here by nightfall."
|
||||
d.title = "Hyenas at the Edge"
|
||||
d.body = "Hyenas howl in the distance. They will be here by nightfall."
|
||||
d.category = EventDef.Category.THREAT
|
||||
d.display = EventDef.Display.MODAL
|
||||
d.cooldown_days = 3
|
||||
|
|
@ -337,8 +337,8 @@ static func _event_13_wolves_at_the_edge() -> EventDef:
|
|||
static func _event_14_lone_wolf() -> EventDef:
|
||||
var d := EventDef.new()
|
||||
d.id = &"lone_wolf"
|
||||
d.title = "Lone Wolf"
|
||||
d.body = "A starving wolf circles your livestock."
|
||||
d.title = "Lone Hyena"
|
||||
d.body = "A starving hyena circles your livestock."
|
||||
d.category = EventDef.Category.THREAT
|
||||
d.display = EventDef.Display.MODAL
|
||||
d.cooldown_days = 3
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue