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
BIN
art/sprites/Hyena_idle.png
Normal file
BIN
art/sprites/Hyena_idle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
40
art/sprites/Hyena_idle.png.import
Normal file
40
art/sprites/Hyena_idle.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bsol1xw7wnmc"
|
||||
path="res://.godot/imported/Hyena_idle.png-998f97e8871db36aec2aedccbd8c28ff.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://art/sprites/Hyena_idle.png"
|
||||
dest_files=["res://.godot/imported/Hyena_idle.png-998f97e8871db36aec2aedccbd8c28ff.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
BIN
art/sprites/Hyena_walk.png
Normal file
BIN
art/sprites/Hyena_walk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
40
art/sprites/Hyena_walk.png.import
Normal file
40
art/sprites/Hyena_walk.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b7gk6ro6ilxa3"
|
||||
path="res://.godot/imported/Hyena_walk.png-5ef407252c6f5cafdb6892139cc44efd.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://art/sprites/Hyena_walk.png"
|
||||
dest_files=["res://.godot/imported/Hyena_walk.png-5ef407252c6f5cafdb6892139cc44efd.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
|
@ -92,7 +92,7 @@ const TABLE: Dictionary = {
|
|||
&"event.no_fire.title": "No Fire",
|
||||
&"event.no_fire.body": "Without a hearth, the cold will bite by night.",
|
||||
&"event.walls.title": "Walls?",
|
||||
&"event.walls.body": "Sleeping under stars is romantic until the wolves arrive.",
|
||||
&"event.walls.body": "Sleeping under stars is romantic until the hyenas arrive.",
|
||||
&"event.spring_awakens.title": "Spring Awakens",
|
||||
&"event.spring_awakens.body": "The thaw runs in every stream. Crops will grow fast now.",
|
||||
&"event.summers_heat.title": "Summer's Heat",
|
||||
|
|
@ -109,10 +109,10 @@ const TABLE: Dictionary = {
|
|||
&"event.the_old_soldier.body": "A retired soldier offers his blade for a place by your fire. Combat 8, but old and tired.",
|
||||
&"event.the_wandering_healer.title": "The Wandering Healer",
|
||||
&"event.the_wandering_healer.body": "A traveling healer asks for shelter. She brings knowledge of medicine.",
|
||||
&"event.wolves_at_the_edge.title": "Wolves at the Edge",
|
||||
&"event.wolves_at_the_edge.body": "Wolves howl in the distance. They will be here by nightfall.",
|
||||
&"event.lone_wolf.title": "Lone Wolf",
|
||||
&"event.lone_wolf.body": "A starving wolf circles your livestock.",
|
||||
&"event.wolves_at_the_edge.title": "Hyenas at the Edge",
|
||||
&"event.wolves_at_the_edge.body": "Hyenas howl in the distance. They will be here by nightfall.",
|
||||
&"event.lone_wolf.title": "Lone Hyena",
|
||||
&"event.lone_wolf.body": "A starving hyena circles your livestock.",
|
||||
&"event.pack_hunt.title": "Pack Hunt",
|
||||
&"event.pack_hunt.body": "A hunting pack moves through the forest. They smell your colony.",
|
||||
&"event.bandit_scouts.title": "Bandit Scouts",
|
||||
|
|
@ -225,7 +225,7 @@ const TABLE: Dictionary = {
|
|||
&"ui.day_summary.title": "Day {day} — {season}",
|
||||
&"ui.day_summary.continue": "Continue",
|
||||
&"ui.day_summary.pawns_alive": "Pawns alive",
|
||||
&"ui.day_summary.wolves_on_map": "Wolves on map",
|
||||
&"ui.day_summary.wolves_on_map": "Hyenas on map",
|
||||
&"ui.day_summary.tension": "Tension",
|
||||
&"ui.day_summary.tension_fmt": "{t} / 100",
|
||||
# Weather labels (weather.<name>)
|
||||
|
|
@ -288,9 +288,9 @@ const TABLE: Dictionary = {
|
|||
&"help.priorities.heading": "Work Priorities",
|
||||
&"help.priorities.body": "Each pawn has priorities (0-4) per work category. The Work matrix grid lets you tune them.\n\n0 = Off. Pawn won't do this work.\n1 = Background. Only if nothing else fits.\n2 = Low.\n3 = Normal (default).\n4 = Urgent. Drops other work to take this.\n\nNeeds (rest, eat, sleep) always run regardless - you can't starve a pawn by setting priorities.",
|
||||
&"help.storyteller.heading": "Storyteller Events",
|
||||
&"help.storyteller.body": "Each in-game day at 6 AM the Storyteller may roll an event. Categories include nudges (gentle hints), threats (wolves, raids), wanderers (recruit offers), seasonal beats, disease, resource booms, lore, and milestones.\n\nThreat events auto-pause the sim and show a modal with a choice. Other events show a banner that fades in a few seconds.\n\nThe Storyteller's tension rises and falls based on what happens to your colony - calmer when nothing has happened recently, escalating after kills, damage, or near-misses.",
|
||||
&"help.storyteller.body": "Each in-game day at 6 AM the Storyteller may roll an event. Categories include nudges (gentle hints), threats (hyenas, raids), wanderers (recruit offers), seasonal beats, disease, resource booms, lore, and milestones.\n\nThreat events auto-pause the sim and show a modal with a choice. Other events show a banner that fades in a few seconds.\n\nThe Storyteller's tension rises and falls based on what happens to your colony - calmer when nothing has happened recently, escalating after kills, damage, or near-misses.",
|
||||
&"help.tips.heading": "Early-game Tips",
|
||||
&"help.tips.body": "Build at least one bed and one wall before nightfall - pawns sleep on the ground and grow tired, and wolves come at night.\n\nKeep beds indoors (under a roof). Rain and cold ruin sleep quality.\n\nPaint a stockpile early - items left on the ground decay (corpses, food) or just clutter the map.\n\nWatch tension. If it climbs, slow down and reinforce. The Storyteller is reactive - give it nothing to react to and it stays quiet.\n\nFood priority order: Meal > Bread > Vegetable / Grain / Strawberry > raw Wheat. Cook before eating raw.",
|
||||
&"help.tips.body": "Build at least one bed and one wall before nightfall - pawns sleep on the ground and grow tired, and hyenas come at night.\n\nKeep beds indoors (under a roof). Rain and cold ruin sleep quality.\n\nPaint a stockpile early - items left on the ground decay (corpses, food) or just clutter the map.\n\nWatch tension. If it climbs, slow down and reinforce. The Storyteller is reactive - give it nothing to react to and it stays quiet.\n\nFood priority order: Meal > Bread > Vegetable / Grain / Strawberry > raw Wheat. Cook before eating raw.",
|
||||
# Phase 19 — Onboarding section in SettingsMenu.
|
||||
&"ui.settings.section.onboarding": "Onboarding",
|
||||
&"ui.settings.show_hints": "Show hints (first session)",
|
||||
|
|
|
|||
|
|
@ -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,6 +253,10 @@ 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)
|
||||
_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()
|
||||
|
||||
|
||||
|
|
@ -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