Конвейер ассетов: манифесты, риги, каталоги, инструменты
Всё, из чего собираются спрайты, и ничего собранного. Источник истины —
assets/manifests и assets/*/catalog.yaml; PNG, .sfa и contact sheet остаются
за бортом, потому что воспроизводятся командой (docs/12-assets.md).
assets/manifests что и как рендерить: окружение, твари, агенты, предметы
assets/agents паперdoll-риг восьми направлений, каталог снаряжения
assets/monsters пять морфологий, каждая своим ригом (docs/14-monsters.md)
assets/sources исходные развёртки и импорты
assets/art_masters мастер-развёртки, по которым сверяется стиль
tools/ сборка ассетов, покрытие, генераторы, blender-скрипты
generated/ заголовки с константами SF_ASSET_*: без них игра не
соберётся, поэтому они в репозитории, хотя и производные
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BIN
assets/agents/agent_rig.blend
Normal file
43
assets/agents/catalog.yaml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
version: 1
|
||||||
|
frame: {width: 70, height: 90, pivot_x: 35, pivot_y: 87}
|
||||||
|
directions: [south, southwest, west, northwest, north, northeast, east, southeast]
|
||||||
|
animations:
|
||||||
|
idle: {frames: 4, fps: 8}
|
||||||
|
walk: {frames: 8, fps: 12}
|
||||||
|
attack: {frames: 6, fps: 12}
|
||||||
|
|
||||||
|
# Одна камера на все компоненты и оба конвейера (Blender и растровый).
|
||||||
|
# elevation_deg = atan(TILE_H / TILE_W) = atan(0.5): ровно тот же 2:1, по
|
||||||
|
# которому в игре лежит пол (src/tuning.h). Разъедься эти два числа — фигура
|
||||||
|
# встала бы к полу под другим углом, чем ромб под ней.
|
||||||
|
camera:
|
||||||
|
elevation_deg: 26.565
|
||||||
|
# Alien-Shooter-like gameplay scale: an upright 1.8 m operator occupies
|
||||||
|
# roughly 28-30 internal pixels, not half the screen.
|
||||||
|
pixels_per_unit: 28.0
|
||||||
|
distance: 10.0 # только для клиппинга: проекция ортографическая
|
||||||
|
body_height: 1.80 # метры, худой взрослый; не бронированный громила
|
||||||
|
source_model: "assets/vendor/quaternius-universal-animation-library/files/Animation Library[Standard]/Godot/AnimationLibrary_Godot_Standard.glb"
|
||||||
|
source_rotation_deg: 0.0
|
||||||
|
source_scale: 0.96
|
||||||
|
source_ground_z: 0.0
|
||||||
|
# Low-resolution silhouette compensation applied after skin deformation.
|
||||||
|
# It preserves the authored skeleton/actions but prevents arms and legs from
|
||||||
|
# collapsing to one-pixel wires in a 70x90 sprite cell.
|
||||||
|
source_silhouette_thickness: 0.022
|
||||||
|
|
||||||
|
# Every component is rendered against the same skeleton/camera. Missing source
|
||||||
|
# files are allowed while authoring; `agent_assets.py validate --complete`
|
||||||
|
# turns them into errors for a release build.
|
||||||
|
components:
|
||||||
|
body_sleek: {slot: body, style: sleek infiltration body}
|
||||||
|
armor_jacket: {slot: armor, style: fitted field jacket and thin tactical harness}
|
||||||
|
armor_vest: {slot: armor, style: light low-profile ballistic vest}
|
||||||
|
armor_plate: {slot: armor, style: segmented medium ceramic chest plates}
|
||||||
|
armor_shroud: {slot: armor, style: close-fitting stealth shroud, no cape}
|
||||||
|
weapon_sword: {slot: weapon, style: narrow tactical longsword}
|
||||||
|
weapon_dagger: {slot: weapon, style: compact combat dagger}
|
||||||
|
weapon_rifle: {slot: weapon, style: compact suppressed assault rifle}
|
||||||
|
weapon_shotgun: {slot: weapon, style: short industrial combat shotgun}
|
||||||
|
weapon_pistol: {slot: weapon, style: suppressed service pistol}
|
||||||
|
weapon_bow: {slot: weapon, style: compact near-future compound bow}
|
||||||
BIN
assets/art_masters/operative_turnaround_chroma_v1.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/art_masters/operative_turnaround_v1.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
assets/art_masters/stalker_turnaround_chroma_v1.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
assets/art_masters/stalker_turnaround_v1.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
60
assets/content_coverage.yaml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
version: 1
|
||||||
|
art_direction:
|
||||||
|
world: "derelict near-future facility, industrial sci-fi horror, invasive wet biomass"
|
||||||
|
agents: "lean covert operators, grounded tactical sci-fi; human, not bulky power armor"
|
||||||
|
enemies: "readable body-horror anatomy, surgically mutated flesh and industrial implants"
|
||||||
|
|
||||||
|
agents:
|
||||||
|
components:
|
||||||
|
body: [body_sleek]
|
||||||
|
armor: [armor_jacket, armor_vest, armor_plate, armor_shroud]
|
||||||
|
weapon: [weapon_sword, weapon_dagger, weapon_rifle, weapon_shotgun, weapon_pistol, weapon_bow]
|
||||||
|
animations: [idle, walk, attack]
|
||||||
|
release_requires_all_combinations: true
|
||||||
|
|
||||||
|
enemies:
|
||||||
|
kinds: [shambler, rusher, spitter, brute, lurker]
|
||||||
|
animations: [walk, attack, hit, death]
|
||||||
|
|
||||||
|
environment:
|
||||||
|
required:
|
||||||
|
- floor_clean
|
||||||
|
- floor_cracked
|
||||||
|
- floor_grate
|
||||||
|
- floor_biomass
|
||||||
|
- wall_industrial
|
||||||
|
- wall_infested
|
||||||
|
- prop_crate
|
||||||
|
- prop_barrel
|
||||||
|
- prop_locker
|
||||||
|
|
||||||
|
item_icons:
|
||||||
|
required:
|
||||||
|
- item_icon_rifle
|
||||||
|
- item_icon_pistol
|
||||||
|
- item_icon_shotgun
|
||||||
|
- item_icon_smg
|
||||||
|
- item_icon_magnum
|
||||||
|
- item_icon_bow
|
||||||
|
- item_icon_saber
|
||||||
|
- item_icon_knife
|
||||||
|
- item_icon_cleaver
|
||||||
|
- item_icon_jacket
|
||||||
|
- item_icon_vest
|
||||||
|
- item_icon_plate
|
||||||
|
- item_icon_shroud
|
||||||
|
- item_icon_lantern
|
||||||
|
- item_icon_charm
|
||||||
|
- item_icon_spurs
|
||||||
|
- item_icon_bandage
|
||||||
|
- item_icon_stimpack
|
||||||
|
|
||||||
|
procedural_runtime_exemptions:
|
||||||
|
# These are temporal primitives, not reusable bitmap assets.
|
||||||
|
- bullet_tracer
|
||||||
|
- muzzle_flash
|
||||||
|
- impact_spark
|
||||||
|
- melee_arc
|
||||||
|
- fog_of_war
|
||||||
|
- blood_field
|
||||||
|
- ui_chrome
|
||||||
56
assets/manifests/agents.blender.yaml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Production agent sources rendered directly by SpriteForge. Project-specific
|
||||||
|
# object names live here; the standalone tool only understands logical layers.
|
||||||
|
version: 1
|
||||||
|
defaults:
|
||||||
|
backend: blender
|
||||||
|
dirs: 8
|
||||||
|
fps: 12
|
||||||
|
size: 70x90
|
||||||
|
postprocess_scale: 0.25
|
||||||
|
pivot: "140,348"
|
||||||
|
tags: [agent, modular, blender]
|
||||||
|
|
||||||
|
agent_body_sleek:
|
||||||
|
rig: ../agents/agent_rig.blend
|
||||||
|
parts: {body: body_sleek_mesh}
|
||||||
|
anims: [idle, walk, attack]
|
||||||
|
params: &agent_render
|
||||||
|
executable: blender
|
||||||
|
render_size: 280x360
|
||||||
|
ortho_scale: 3.2142857
|
||||||
|
elevation: 26.565
|
||||||
|
camera_distance: 10
|
||||||
|
# With the 26.565 degree camera this projects world origin (the point
|
||||||
|
# between the feet) to the declared supersampled pivot at y=348.
|
||||||
|
camera_target: [0, 0, 1.70]
|
||||||
|
direction_offset_deg: -90
|
||||||
|
engine: BLENDER_EEVEE
|
||||||
|
samples: 16
|
||||||
|
actions: {idle: idle, walk: walk, attack: attack}
|
||||||
|
frames: {idle: 4, walk: 8, attack: 6}
|
||||||
|
loop_sampling: true
|
||||||
|
part_members:
|
||||||
|
body: [body_sleek_mesh, body_sleek_helmet, body_sleek_visor,
|
||||||
|
body_sleek_respirator, body_sleek_collar]
|
||||||
|
|
||||||
|
agent_armor_vest:
|
||||||
|
tags: [agent, modular, blender, armor, paperdoll_layer]
|
||||||
|
rig: ../agents/agent_rig.blend
|
||||||
|
parts: {armor: armor_vest_shell}
|
||||||
|
anims: [idle, walk, attack]
|
||||||
|
params:
|
||||||
|
<<: *agent_render
|
||||||
|
part_members:
|
||||||
|
armor: [armor_vest_shell, armor_vest_chest_plate,
|
||||||
|
armor_vest_mag_l, armor_vest_mag_r]
|
||||||
|
|
||||||
|
agent_weapon_rifle:
|
||||||
|
tags: [agent, modular, blender, weapon, paperdoll_layer]
|
||||||
|
rig: ../agents/agent_rig.blend
|
||||||
|
parts: {weapon: weapon_rifle_receiver}
|
||||||
|
anims: [idle, walk, attack]
|
||||||
|
params:
|
||||||
|
<<: *agent_render
|
||||||
|
part_members:
|
||||||
|
weapon: [weapon_rifle_receiver, weapon_rifle_barrel,
|
||||||
|
weapon_rifle_stock, weapon_rifle_optic]
|
||||||
239
assets/manifests/agents.generated.yaml
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
# Собрано `python tools/agent_assets.py sync`.
|
||||||
|
# Записи с backend: import перегенерируются по PNG в assets/sources/agents/sleek/.
|
||||||
|
# Записи с любым другим backend (например blender) sync СОХРАНЯЕТ как есть:
|
||||||
|
# правь их руками здесь, это единственное место, где такая правка переживает sync.
|
||||||
|
version: 1
|
||||||
|
defaults:
|
||||||
|
dirs: 8
|
||||||
|
pivot: "35,87"
|
||||||
|
|
||||||
|
agent_body_sleek_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/body_sleek_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, body, idle]
|
||||||
|
|
||||||
|
agent_body_sleek_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/body_sleek_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, body, walk]
|
||||||
|
|
||||||
|
agent_body_sleek_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/body_sleek_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, body, attack]
|
||||||
|
|
||||||
|
agent_armor_jacket_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_jacket_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, armor, idle, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_jacket_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_jacket_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, armor, walk, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_jacket_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_jacket_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, armor, attack, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_vest_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_vest_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, armor, idle, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_vest_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_vest_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, armor, walk, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_vest_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_vest_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, armor, attack, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_plate_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_plate_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, armor, idle, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_plate_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_plate_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, armor, walk, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_plate_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_plate_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, armor, attack, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_shroud_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_shroud_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, armor, idle, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_shroud_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_shroud_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, armor, walk, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_armor_shroud_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/armor_shroud_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, armor, attack, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_sword_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_sword_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, weapon, idle, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_sword_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_sword_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, weapon, walk, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_sword_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_sword_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, weapon, attack, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_dagger_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_dagger_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, weapon, idle, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_dagger_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_dagger_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, weapon, walk, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_dagger_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_dagger_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, weapon, attack, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_rifle_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_rifle_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, weapon, idle, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_rifle_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_rifle_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, weapon, walk, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_rifle_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_rifle_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, weapon, attack, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_shotgun_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_shotgun_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, weapon, idle, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_shotgun_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_shotgun_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, weapon, walk, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_shotgun_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_shotgun_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, weapon, attack, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_pistol_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_pistol_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, weapon, idle, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_pistol_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_pistol_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, weapon, walk, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_pistol_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_pistol_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, weapon, attack, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_bow_idle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_bow_idle.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 32}
|
||||||
|
tags: [agent, weapon, idle, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_bow_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_bow_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 64}
|
||||||
|
tags: [agent, weapon, walk, paperdoll_layer]
|
||||||
|
|
||||||
|
agent_weapon_bow_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/agents/sleek/weapon_bow_attack.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 70, frame_height: 90, frames: 48}
|
||||||
|
tags: [agent, weapon, attack, paperdoll_layer]
|
||||||
29
assets/manifests/diagnostics/agent_blender_gate.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# One-frame visual/geometry gate for iterating on the production agent rig.
|
||||||
|
# It is intentionally outside the normal manifest set and builds in seconds.
|
||||||
|
version: 1
|
||||||
|
agent_body_gate:
|
||||||
|
backend: blender
|
||||||
|
rig: ../../agents/agent_rig.blend
|
||||||
|
parts: {body: body_sleek_mesh}
|
||||||
|
anims: [idle]
|
||||||
|
dirs: 1
|
||||||
|
fps: 8
|
||||||
|
size: 70x90
|
||||||
|
postprocess_scale: 0.25
|
||||||
|
pivot: "140,348"
|
||||||
|
tags: [diagnostic, agent, blender]
|
||||||
|
params:
|
||||||
|
executable: blender
|
||||||
|
render_size: 280x360
|
||||||
|
ortho_scale: 3.2142857
|
||||||
|
elevation: 26.565
|
||||||
|
camera_distance: 10
|
||||||
|
camera_target: [0, 0, 1.70]
|
||||||
|
direction_offset_deg: -90
|
||||||
|
engine: BLENDER_EEVEE
|
||||||
|
samples: 16
|
||||||
|
actions: {idle: idle}
|
||||||
|
frames: {idle: 1}
|
||||||
|
part_members:
|
||||||
|
body: [body_sleek_mesh, body_sleek_helmet, body_sleek_visor,
|
||||||
|
body_sleek_respirator, body_sleek_collar]
|
||||||
26
assets/manifests/diagnostics/monster_blender_gate.yaml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
version: 1
|
||||||
|
rusher_gate:
|
||||||
|
backend: blender
|
||||||
|
rig: ../../monsters/rusher.blend
|
||||||
|
parts: {body: unused_when_prefix_is_set}
|
||||||
|
anims: [walk]
|
||||||
|
dirs: 1
|
||||||
|
fps: 14
|
||||||
|
size: 96x112
|
||||||
|
postprocess_scale: 0.5
|
||||||
|
pivot: "96,176"
|
||||||
|
tags: [diagnostic, enemy, blender]
|
||||||
|
params:
|
||||||
|
executable: blender
|
||||||
|
render_size: 192x224
|
||||||
|
ortho_scale: 3.5
|
||||||
|
elevation: 26.565
|
||||||
|
camera_distance: 12
|
||||||
|
camera_target: [0, 0, 1.118]
|
||||||
|
direction_offset_deg: -90
|
||||||
|
engine: BLENDER_EEVEE
|
||||||
|
samples: 12
|
||||||
|
actions: {walk: rusher_walk}
|
||||||
|
frames: {walk: 1}
|
||||||
|
loop_sampling: {walk: true}
|
||||||
|
part_prefixes: {body: rusher_}
|
||||||
8
assets/manifests/enemies.yaml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Legacy placeholder manifest retained so old build commands keep working.
|
||||||
|
# Real eight-direction body-horror enemies are generated into
|
||||||
|
# monsters.generated.yaml by tools/monster_assets.py.
|
||||||
|
version: 1
|
||||||
|
defaults:
|
||||||
|
dirs: 1
|
||||||
|
fps: 10
|
||||||
|
pivot: bottom_center
|
||||||
31
assets/manifests/environment.yaml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Тайлы уровня. Геометрия обязана совпадать с src/tuning.h:
|
||||||
|
# ромб пола TILE_W x TILE_H = 32 x 16
|
||||||
|
# блок стены TILE_W x WALL_H = 32 x 32 (крыша 16 + вертикальные грани 16)
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
dirs: 1
|
||||||
|
fps: 12
|
||||||
|
pivot: bottom_center
|
||||||
|
|
||||||
|
# Шахматка пола — два варианта. Ромб 32x16, pivot по центру ромба.
|
||||||
|
floor_slab:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/imports/floor_slab.png
|
||||||
|
pivot: "15,8"
|
||||||
|
tags: [environment, floor]
|
||||||
|
|
||||||
|
floor_slab_dim:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/imports/floor_slab_dim.png
|
||||||
|
pivot: "15,8"
|
||||||
|
tags: [environment, floor]
|
||||||
|
|
||||||
|
# Блок стены: верхняя грань 32x16 + боковые грани 16px вниз = 32x32.
|
||||||
|
# pivot — нижний край блока (низ боковых граней), anchor в C++ перекладывает
|
||||||
|
# его на центр крыши (TILE_HALF_W, TILE_HALF_H + TILE_H).
|
||||||
|
wall_block:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/imports/wall_block.png
|
||||||
|
pivot: "15,31"
|
||||||
|
tags: [environment, wall]
|
||||||
71
assets/manifests/monsters.blender.yaml
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# Five production monster rigs rendered directly by SpriteForge. Each asset
|
||||||
|
# contains every gameplay clip; no intermediate sprite sheets are required.
|
||||||
|
version: 1
|
||||||
|
defaults:
|
||||||
|
backend: blender
|
||||||
|
dirs: 8
|
||||||
|
fps: 12
|
||||||
|
size: 96x112
|
||||||
|
postprocess_scale: 0.5
|
||||||
|
pivot: "96,176"
|
||||||
|
tags: [enemy, body_horror, blender, consolidated]
|
||||||
|
|
||||||
|
shambler:
|
||||||
|
rig: ../monsters/shambler.blend
|
||||||
|
parts: {body: unused_when_prefix_is_set}
|
||||||
|
anims: [walk, attack, hit, death]
|
||||||
|
params: &monster_render
|
||||||
|
executable: blender
|
||||||
|
render_size: 192x224
|
||||||
|
ortho_scale: 3.5
|
||||||
|
elevation: 26.565
|
||||||
|
camera_distance: 12
|
||||||
|
camera_target: [0, 0, 1.118]
|
||||||
|
direction_offset_deg: -90
|
||||||
|
engine: BLENDER_EEVEE
|
||||||
|
samples: 12
|
||||||
|
frames: {walk: 8, attack: 6, hit: 4, death: 8}
|
||||||
|
animation_fps: {walk: 8, attack: 10, hit: 11, death: 8}
|
||||||
|
loop_sampling: {walk: true, attack: false, hit: false, death: false}
|
||||||
|
actions: {walk: shambler_walk, attack: shambler_attack, hit: shambler_hit, death: shambler_death}
|
||||||
|
part_prefixes: {body: shambler_}
|
||||||
|
|
||||||
|
rusher:
|
||||||
|
rig: ../monsters/rusher.blend
|
||||||
|
parts: {body: unused_when_prefix_is_set}
|
||||||
|
anims: [walk, attack, hit, death]
|
||||||
|
params:
|
||||||
|
<<: *monster_render
|
||||||
|
animation_fps: {walk: 14, attack: 17, hit: 20, death: 14}
|
||||||
|
actions: {walk: rusher_walk, attack: rusher_attack, hit: rusher_hit, death: rusher_death}
|
||||||
|
part_prefixes: {body: rusher_}
|
||||||
|
|
||||||
|
spitter:
|
||||||
|
rig: ../monsters/spitter.blend
|
||||||
|
parts: {body: unused_when_prefix_is_set}
|
||||||
|
anims: [walk, attack, hit, death]
|
||||||
|
params:
|
||||||
|
<<: *monster_render
|
||||||
|
animation_fps: {walk: 9, attack: 11, hit: 13, death: 9}
|
||||||
|
actions: {walk: spitter_walk, attack: spitter_attack, hit: spitter_hit, death: spitter_death}
|
||||||
|
part_prefixes: {body: spitter_}
|
||||||
|
|
||||||
|
brute:
|
||||||
|
rig: ../monsters/brute.blend
|
||||||
|
parts: {body: unused_when_prefix_is_set}
|
||||||
|
anims: [walk, attack, hit, death]
|
||||||
|
params:
|
||||||
|
<<: *monster_render
|
||||||
|
animation_fps: {walk: 7, attack: 8, hit: 10, death: 7}
|
||||||
|
actions: {walk: brute_walk, attack: brute_attack, hit: brute_hit, death: brute_death}
|
||||||
|
part_prefixes: {body: brute_}
|
||||||
|
|
||||||
|
lurker:
|
||||||
|
rig: ../monsters/lurker.blend
|
||||||
|
parts: {body: unused_when_prefix_is_set}
|
||||||
|
anims: [walk, attack, hit, death]
|
||||||
|
params:
|
||||||
|
<<: *monster_render
|
||||||
|
animation_fps: {walk: 12, attack: 14, hit: 17, death: 12}
|
||||||
|
actions: {walk: lurker_walk, attack: lurker_attack, hit: lurker_hit, death: lurker_death}
|
||||||
|
part_prefixes: {body: lurker_}
|
||||||
151
assets/manifests/monsters.generated.yaml
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# Собрано `python tools/monster_assets.py sync`.
|
||||||
|
# Записи с backend: import перегенерируются по PNG в assets/sources/monsters/.
|
||||||
|
# Записи с любым другим backend sync СОХРАНЯЕТ как есть: правь их руками
|
||||||
|
# здесь, это единственное место, где такая правка переживает sync.
|
||||||
|
#
|
||||||
|
# ID ассета — <kind>_<animation>: ровно то, что требует
|
||||||
|
# tools/asset_coverage.py от группы enemy. Переименуешь — упадёт покрытие.
|
||||||
|
version: 1
|
||||||
|
defaults:
|
||||||
|
dirs: 8
|
||||||
|
pivot: "48,88"
|
||||||
|
|
||||||
|
shambler_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/shambler_walk.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 64}
|
||||||
|
tags: [enemy, shambler, walk]
|
||||||
|
|
||||||
|
shambler_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/shambler_attack.png
|
||||||
|
fps: 10
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 48}
|
||||||
|
tags: [enemy, shambler, attack]
|
||||||
|
|
||||||
|
shambler_hit:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/shambler_hit.png
|
||||||
|
fps: 11
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 32}
|
||||||
|
tags: [enemy, shambler, hit]
|
||||||
|
|
||||||
|
shambler_death:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/shambler_death.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 64}
|
||||||
|
tags: [enemy, shambler, death]
|
||||||
|
|
||||||
|
rusher_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/rusher_walk.png
|
||||||
|
fps: 14
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 64}
|
||||||
|
tags: [enemy, rusher, walk]
|
||||||
|
|
||||||
|
rusher_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/rusher_attack.png
|
||||||
|
fps: 17
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 48}
|
||||||
|
tags: [enemy, rusher, attack]
|
||||||
|
|
||||||
|
rusher_hit:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/rusher_hit.png
|
||||||
|
fps: 20
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 32}
|
||||||
|
tags: [enemy, rusher, hit]
|
||||||
|
|
||||||
|
rusher_death:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/rusher_death.png
|
||||||
|
fps: 14
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 64}
|
||||||
|
tags: [enemy, rusher, death]
|
||||||
|
|
||||||
|
spitter_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/spitter_walk.png
|
||||||
|
fps: 9
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 64}
|
||||||
|
tags: [enemy, spitter, walk]
|
||||||
|
|
||||||
|
spitter_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/spitter_attack.png
|
||||||
|
fps: 11
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 48}
|
||||||
|
tags: [enemy, spitter, attack]
|
||||||
|
|
||||||
|
spitter_hit:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/spitter_hit.png
|
||||||
|
fps: 13
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 32}
|
||||||
|
tags: [enemy, spitter, hit]
|
||||||
|
|
||||||
|
spitter_death:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/spitter_death.png
|
||||||
|
fps: 9
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 64}
|
||||||
|
tags: [enemy, spitter, death]
|
||||||
|
|
||||||
|
brute_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/brute_walk.png
|
||||||
|
fps: 7
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 64}
|
||||||
|
tags: [enemy, brute, walk]
|
||||||
|
|
||||||
|
brute_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/brute_attack.png
|
||||||
|
fps: 8
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 48}
|
||||||
|
tags: [enemy, brute, attack]
|
||||||
|
|
||||||
|
brute_hit:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/brute_hit.png
|
||||||
|
fps: 10
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 32}
|
||||||
|
tags: [enemy, brute, hit]
|
||||||
|
|
||||||
|
brute_death:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/brute_death.png
|
||||||
|
fps: 7
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 64}
|
||||||
|
tags: [enemy, brute, death]
|
||||||
|
|
||||||
|
lurker_walk:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/lurker_walk.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 64}
|
||||||
|
tags: [enemy, lurker, walk]
|
||||||
|
|
||||||
|
lurker_attack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/lurker_attack.png
|
||||||
|
fps: 14
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 48}
|
||||||
|
tags: [enemy, lurker, attack]
|
||||||
|
|
||||||
|
lurker_hit:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/lurker_hit.png
|
||||||
|
fps: 17
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 32}
|
||||||
|
tags: [enemy, lurker, hit]
|
||||||
|
|
||||||
|
lurker_death:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/monsters/lurker_death.png
|
||||||
|
fps: 12
|
||||||
|
params: {frame_width: 96, frame_height: 112, frames: 64}
|
||||||
|
tags: [enemy, lurker, death]
|
||||||
166
assets/manifests/static.generated.yaml
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
version: 1
|
||||||
|
defaults:
|
||||||
|
dirs: 1
|
||||||
|
fps: 1
|
||||||
|
|
||||||
|
floor_clean:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/world/floor_clean.png
|
||||||
|
pivot: "15,8"
|
||||||
|
tags: [environment, floor]
|
||||||
|
|
||||||
|
floor_cracked:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/world/floor_cracked.png
|
||||||
|
pivot: "15,8"
|
||||||
|
tags: [environment, floor]
|
||||||
|
|
||||||
|
floor_grate:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/world/floor_grate.png
|
||||||
|
pivot: "15,8"
|
||||||
|
tags: [environment, floor]
|
||||||
|
|
||||||
|
floor_biomass:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/world/floor_biomass.png
|
||||||
|
pivot: "15,8"
|
||||||
|
tags: [environment, floor]
|
||||||
|
|
||||||
|
wall_industrial:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/world/wall_industrial.png
|
||||||
|
pivot: "15,31"
|
||||||
|
tags: [environment, wall]
|
||||||
|
|
||||||
|
wall_infested:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/world/wall_infested.png
|
||||||
|
pivot: "15,31"
|
||||||
|
tags: [environment, wall]
|
||||||
|
|
||||||
|
prop_crate:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/world/prop_crate.png
|
||||||
|
pivot: bottom_center
|
||||||
|
tags: [environment, prop]
|
||||||
|
|
||||||
|
prop_barrel:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/world/prop_barrel.png
|
||||||
|
pivot: bottom_center
|
||||||
|
tags: [environment, prop]
|
||||||
|
|
||||||
|
prop_locker:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/world/prop_locker.png
|
||||||
|
pivot: bottom_center
|
||||||
|
tags: [environment, prop]
|
||||||
|
|
||||||
|
item_icon_rifle:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_rifle.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_pistol:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_pistol.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_shotgun:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_shotgun.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_smg:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_smg.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_magnum:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_magnum.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_bow:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_bow.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_saber:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_saber.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_knife:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_knife.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_cleaver:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_cleaver.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_jacket:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_jacket.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_vest:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_vest.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_plate:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_plate.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_shroud:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_shroud.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_lantern:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_lantern.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_charm:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_charm.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_spurs:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_spurs.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_bandage:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_bandage.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
|
|
||||||
|
item_icon_stimpack:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/items/item_icon_stimpack.png
|
||||||
|
pivot: center
|
||||||
|
tags: [ui, item]
|
||||||
BIN
assets/monsters/brute.blend
Normal file
62
assets/monsters/catalog.yaml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Каталог тварей: единственный источник геометрии monster-конвейера.
|
||||||
|
#
|
||||||
|
# Твари — НЕ бумажная кукла. У агента слои обязаны совпадать пиксель в пиксель,
|
||||||
|
# поэтому там один силуэт на всех. Здесь наоборот: пять видов обязаны читаться
|
||||||
|
# по пятну, не глядя на детали, — значит у каждого своя морфология, свой рост и
|
||||||
|
# своя походка. Общее у них ровно две вещи: камера и точка привязки.
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
# Ячейка листа и общий pivot. pivot_y НЕ на нижней кромке: изометрический пол
|
||||||
|
# уходит от точки опоры ВНИЗ по экрану, и всё, что у твари позади (хвост
|
||||||
|
# бегуна, задние лапы затаившегося), проецируется ниже точки опоры. Девять
|
||||||
|
# строк под pivot — это запас на глубину подставки, а не «воздух».
|
||||||
|
# Monsters may sprawl wider than the 70x90 human paper-doll when dying. The
|
||||||
|
# camera scale stays unchanged: this is transparent safety padding, not a
|
||||||
|
# larger creature. Human agent layers remain exactly 70x90.
|
||||||
|
frame: {width: 96, height: 112, pivot_x: 48, pivot_y: 88}
|
||||||
|
|
||||||
|
# Порядок направлений тот же, что у агентов: по -45° от юга.
|
||||||
|
directions: [south, southwest, west, northwest, north, northeast, east, southeast]
|
||||||
|
|
||||||
|
# Кадры на ОДНО направление. Лист direction-major: сначала все кадры юга,
|
||||||
|
# затем юго-запада и так далее.
|
||||||
|
animations:
|
||||||
|
walk: {frames: 8, fps: 10}
|
||||||
|
attack: {frames: 6, fps: 12}
|
||||||
|
hit: {frames: 4, fps: 14}
|
||||||
|
death: {frames: 8, fps: 10}
|
||||||
|
|
||||||
|
# Камера ровно та же, что у агентов: elevation_deg = atan(TILE_H / TILE_W) =
|
||||||
|
# atan(0.5). Разъедься эти числа — тварь встала бы к полу под другим углом,
|
||||||
|
# чем боец рядом с ней.
|
||||||
|
camera:
|
||||||
|
elevation_deg: 26.565
|
||||||
|
# Common mutants read slightly larger than an operator; brute dominates.
|
||||||
|
pixels_per_unit: 32.0
|
||||||
|
distance: 12.0 # только для клиппинга: проекция ортографическая
|
||||||
|
|
||||||
|
# height_m — справочная высота силуэта в метрах, ею меряется читаемость
|
||||||
|
# размеров: громила обязан быть вдвое выше затаившегося.
|
||||||
|
# fps_scale — темп клипа относительно animations.fps: бредущий волочится,
|
||||||
|
# бегун частит. Ассету это даёт разный fps при одинаковом числе кадров.
|
||||||
|
kinds:
|
||||||
|
shambler:
|
||||||
|
style: "flayed penitent; thorax cranked open, ribs splayed outward, one arm grown past the knee"
|
||||||
|
height_m: 1.58
|
||||||
|
fps_scale: 0.8
|
||||||
|
rusher:
|
||||||
|
style: "quadruped sprinter; horizontal spine, digitigrade hind legs, scythe forelimbs, headless split-jaw bloom"
|
||||||
|
height_m: 0.78
|
||||||
|
fps_scale: 1.4
|
||||||
|
spitter:
|
||||||
|
style: "pressure vessel; dragging gland sac, implanted throat valve and nozzle, vestigial arms"
|
||||||
|
height_m: 1.22
|
||||||
|
fps_scale: 0.9
|
||||||
|
brute:
|
||||||
|
style: "the fused; a second half-absorbed body grafted to the shoulder, bolted plating, hammer of fused bone"
|
||||||
|
height_m: 1.96
|
||||||
|
fps_scale: 0.7
|
||||||
|
lurker:
|
||||||
|
style: "arachnid harvester; flat sutured carapace slung under four long arching limbs, eyeless hook wedge"
|
||||||
|
height_m: 0.74
|
||||||
|
fps_scale: 1.2
|
||||||
BIN
assets/monsters/lurker.blend
Normal file
BIN
assets/monsters/monster_rig.blend
Normal file
BIN
assets/monsters/rusher.blend
Normal file
BIN
assets/monsters/shambler.blend
Normal file
BIN
assets/monsters/spitter.blend
Normal file
BIN
assets/references/alien-shooter-gameplay.jpg
Normal file
|
After Width: | Height: | Size: 362 KiB |
21
assets/sources/README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# assets/sources
|
||||||
|
|
||||||
|
Исходники ассетов, на которые ссылаются манифесты из `assets/manifests/`.
|
||||||
|
|
||||||
|
- `imports/` — готовые PNG для бэкенда `import` (поле `source`);
|
||||||
|
- `rigs/` — `.blend`-риги для бэкенда `blender` (поле `rig`).
|
||||||
|
|
||||||
|
Процедурные ассеты исходников не имеют: генератор и параметры целиком описаны
|
||||||
|
в манифесте. Пути в манифесте разрешаются относительно самого манифеста,
|
||||||
|
поэтому импорт выглядит так:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
health_potion:
|
||||||
|
backend: import
|
||||||
|
source: ../sources/imports/health_potion.png
|
||||||
|
size: 32x32
|
||||||
|
tags: [item, consumable]
|
||||||
|
```
|
||||||
|
|
||||||
|
Собранные `.sfa` и `palettes.sfp` сюда не кладутся — они живут в `build/sprites/`
|
||||||
|
и не версионируются. Подробности: [docs/12-assets.md](../../docs/12-assets.md).
|
||||||
BIN
assets/sources/agents/sleek/armor_jacket_attack.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/sources/agents/sleek/armor_jacket_idle.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
assets/sources/agents/sleek/armor_jacket_walk.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/sources/agents/sleek/armor_plate_attack.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
assets/sources/agents/sleek/armor_plate_idle.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
assets/sources/agents/sleek/armor_plate_walk.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/sources/agents/sleek/armor_shroud_attack.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
assets/sources/agents/sleek/armor_shroud_idle.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/sources/agents/sleek/armor_shroud_walk.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
assets/sources/agents/sleek/armor_vest_attack.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
assets/sources/agents/sleek/armor_vest_idle.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/sources/agents/sleek/armor_vest_walk.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
assets/sources/agents/sleek/body_sleek_attack.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
assets/sources/agents/sleek/body_sleek_idle.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/sources/agents/sleek/body_sleek_walk.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
assets/sources/agents/sleek/weapon_bow_attack.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/sources/agents/sleek/weapon_bow_idle.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
assets/sources/agents/sleek/weapon_bow_walk.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/sources/agents/sleek/weapon_dagger_attack.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
assets/sources/agents/sleek/weapon_dagger_idle.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/sources/agents/sleek/weapon_dagger_walk.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
assets/sources/agents/sleek/weapon_pistol_attack.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/sources/agents/sleek/weapon_pistol_idle.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/sources/agents/sleek/weapon_pistol_walk.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
assets/sources/agents/sleek/weapon_rifle_attack.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/sources/agents/sleek/weapon_rifle_idle.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/sources/agents/sleek/weapon_rifle_walk.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
assets/sources/agents/sleek/weapon_shotgun_attack.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/sources/agents/sleek/weapon_shotgun_idle.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/sources/agents/sleek/weapon_shotgun_walk.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
assets/sources/agents/sleek/weapon_sword_attack.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/sources/agents/sleek/weapon_sword_idle.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/sources/agents/sleek/weapon_sword_walk.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/sources/imports/brute_walk.png
Normal file
|
After Width: | Height: | Size: 823 B |
BIN
assets/sources/imports/floor_slab.png
Normal file
|
After Width: | Height: | Size: 611 B |
BIN
assets/sources/imports/floor_slab_dim.png
Normal file
|
After Width: | Height: | Size: 607 B |
BIN
assets/sources/imports/lurker_walk.png
Normal file
|
After Width: | Height: | Size: 469 B |
BIN
assets/sources/imports/rusher_walk.png
Normal file
|
After Width: | Height: | Size: 453 B |
BIN
assets/sources/imports/shambler_walk.png
Normal file
|
After Width: | Height: | Size: 484 B |
BIN
assets/sources/imports/spitter_walk.png
Normal file
|
After Width: | Height: | Size: 463 B |
BIN
assets/sources/imports/wall_block.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/sources/items/item_icon_bandage.png
Normal file
|
After Width: | Height: | Size: 222 B |
BIN
assets/sources/items/item_icon_bow.png
Normal file
|
After Width: | Height: | Size: 221 B |
BIN
assets/sources/items/item_icon_charm.png
Normal file
|
After Width: | Height: | Size: 265 B |
BIN
assets/sources/items/item_icon_cleaver.png
Normal file
|
After Width: | Height: | Size: 274 B |
BIN
assets/sources/items/item_icon_jacket.png
Normal file
|
After Width: | Height: | Size: 242 B |
BIN
assets/sources/items/item_icon_knife.png
Normal file
|
After Width: | Height: | Size: 258 B |
BIN
assets/sources/items/item_icon_lantern.png
Normal file
|
After Width: | Height: | Size: 265 B |
BIN
assets/sources/items/item_icon_magnum.png
Normal file
|
After Width: | Height: | Size: 196 B |
BIN
assets/sources/items/item_icon_pistol.png
Normal file
|
After Width: | Height: | Size: 196 B |
BIN
assets/sources/items/item_icon_plate.png
Normal file
|
After Width: | Height: | Size: 242 B |
BIN
assets/sources/items/item_icon_rifle.png
Normal file
|
After Width: | Height: | Size: 200 B |
BIN
assets/sources/items/item_icon_saber.png
Normal file
|
After Width: | Height: | Size: 274 B |
BIN
assets/sources/items/item_icon_shotgun.png
Normal file
|
After Width: | Height: | Size: 230 B |
BIN
assets/sources/items/item_icon_shroud.png
Normal file
|
After Width: | Height: | Size: 242 B |
BIN
assets/sources/items/item_icon_smg.png
Normal file
|
After Width: | Height: | Size: 197 B |
BIN
assets/sources/items/item_icon_spurs.png
Normal file
|
After Width: | Height: | Size: 264 B |
BIN
assets/sources/items/item_icon_stimpack.png
Normal file
|
After Width: | Height: | Size: 222 B |
BIN
assets/sources/items/item_icon_vest.png
Normal file
|
After Width: | Height: | Size: 242 B |
292
assets/sources/make_enemy_sheets.py
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
"""Генератор листов анимации тварей для SpriteForge (бэкенд import).
|
||||||
|
|
||||||
|
Зачем скрипт, а не нарисованные руками PNG: силуэт твари обязан совпадать с
|
||||||
|
процедурным запасным вариантом из src/render/sprites.cpp — иначе переключение
|
||||||
|
--sf-enemies меняло бы не «плавность», а саму тварь, и игрок переставал бы её
|
||||||
|
узнавать. Поэтому здесь ровно та же геометрия (объединение эллипсов, свет по
|
||||||
|
верхне-левой кромке, обводка по силуэту), только конечности сдвигаются по фазе
|
||||||
|
шага.
|
||||||
|
|
||||||
|
Скрипт одноразовый и детерминированный: ни одного случайного числа, тот же
|
||||||
|
хеш-шум, что в C++. Перегенерация даёт побитово тот же PNG.
|
||||||
|
|
||||||
|
python assets/sources/make_enemy_sheets.py
|
||||||
|
|
||||||
|
Пишет листы в assets/sources/imports/<kind>_walk.png — по кадрам в ряд.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
OUT_DIR = Path(__file__).resolve().parent / "imports"
|
||||||
|
|
||||||
|
FRAMES = 6 # кадров в цикле шага
|
||||||
|
|
||||||
|
|
||||||
|
# --- цвет --------------------------------------------------------------------
|
||||||
|
# Тот же ShadeColor, что в src/render/sprites.cpp: множитель по RGB с обрезкой.
|
||||||
|
def shade(rgb: tuple[int, int, int], k: float) -> tuple[int, int, int]:
|
||||||
|
return tuple(max(0, min(255, int(c * k))) for c in rgb)
|
||||||
|
|
||||||
|
|
||||||
|
# Хеш из src/core/hash.h. Нужен, чтобы рваный низ у твари совпадал с
|
||||||
|
# процедурным вариантом кадр в кадр.
|
||||||
|
def hash2(a: int, b: int) -> int:
|
||||||
|
h = (a * 0x9E3779B1) & 0xFFFFFFFF
|
||||||
|
h ^= (b + 0x85EBCA6B + ((h << 6) & 0xFFFFFFFF) + (h >> 2)) & 0xFFFFFFFF
|
||||||
|
h &= 0xFFFFFFFF
|
||||||
|
h ^= h >> 15
|
||||||
|
h = (h * 0x2C1B3C6D) & 0xFFFFFFFF
|
||||||
|
h ^= h >> 12
|
||||||
|
return h
|
||||||
|
|
||||||
|
|
||||||
|
def hash01(h: int) -> float:
|
||||||
|
return (h & 0xFFFFFF) / float(0x1000000)
|
||||||
|
|
||||||
|
|
||||||
|
# --- силуэт ------------------------------------------------------------------
|
||||||
|
class Canvas:
|
||||||
|
def __init__(self, w: int, h: int) -> None:
|
||||||
|
self.w, self.h = w, h
|
||||||
|
self.px: list[list[tuple[int, int, int] | None]] = [[None] * w for _ in range(h)]
|
||||||
|
|
||||||
|
def get(self, x: int, y: int):
|
||||||
|
if x < 0 or y < 0 or x >= self.w or y >= self.h:
|
||||||
|
return None
|
||||||
|
return self.px[y][x]
|
||||||
|
|
||||||
|
def set(self, x: int, y: int, c) -> None:
|
||||||
|
if 0 <= x < self.w and 0 <= y < self.h:
|
||||||
|
self.px[y][x] = c
|
||||||
|
|
||||||
|
def paint(self, x: int, y: int, c) -> None:
|
||||||
|
"""Записать только по уже занятому пикселю: силуэт не расширяется."""
|
||||||
|
if self.get(x, y) is not None:
|
||||||
|
self.set(x, y, c)
|
||||||
|
|
||||||
|
def image(self) -> Image.Image:
|
||||||
|
img = Image.new("RGBA", (self.w, self.h), (0, 0, 0, 0))
|
||||||
|
out = img.load()
|
||||||
|
for y in range(self.h):
|
||||||
|
for x in range(self.w):
|
||||||
|
c = self.px[y][x]
|
||||||
|
if c is not None:
|
||||||
|
out[x, y] = (c[0], c[1], c[2], 255)
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def fill_blobs(cv: Canvas, blobs, body) -> None:
|
||||||
|
for y in range(cv.h):
|
||||||
|
for x in range(cv.w):
|
||||||
|
for cx, cy, rx, ry in blobs:
|
||||||
|
dx = (x - cx) / max(0.5, rx)
|
||||||
|
dy = (y - cy) / max(0.5, ry)
|
||||||
|
if dx * dx + dy * dy <= 1.0:
|
||||||
|
cv.set(x, y, body)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def shade_silhouette(cv: Canvas, body) -> None:
|
||||||
|
"""Объём по краю: свет сверху-слева. Копия ShadeSilhouette из sprites.cpp."""
|
||||||
|
hilite, shadow, deep = shade(body, 1.32), shade(body, 0.66), shade(body, 0.48)
|
||||||
|
snapshot = [row[:] for row in cv.px]
|
||||||
|
|
||||||
|
def at(x: int, y: int):
|
||||||
|
if x < 0 or y < 0 or x >= cv.w or y >= cv.h:
|
||||||
|
return None
|
||||||
|
return snapshot[y][x]
|
||||||
|
|
||||||
|
for y in range(cv.h):
|
||||||
|
for x in range(cv.w):
|
||||||
|
if at(x, y) is None:
|
||||||
|
continue
|
||||||
|
lit = at(x - 2, y - 1) is None
|
||||||
|
dark = at(x + 2, y + 1) is None
|
||||||
|
if lit and not dark:
|
||||||
|
cv.set(x, y, hilite)
|
||||||
|
elif dark and not lit:
|
||||||
|
cv.set(x, y, shadow)
|
||||||
|
elif dark:
|
||||||
|
cv.set(x, y, deep)
|
||||||
|
|
||||||
|
|
||||||
|
def outline(cv: Canvas, col) -> None:
|
||||||
|
snapshot = [row[:] for row in cv.px]
|
||||||
|
|
||||||
|
def at(x: int, y: int):
|
||||||
|
if x < 0 or y < 0 or x >= cv.w or y >= cv.h:
|
||||||
|
return None
|
||||||
|
return snapshot[y][x]
|
||||||
|
|
||||||
|
for y in range(cv.h):
|
||||||
|
for x in range(cv.w):
|
||||||
|
if at(x, y) is None:
|
||||||
|
continue
|
||||||
|
if (x == 0 or y == 0 or x == cv.w - 1 or y == cv.h - 1
|
||||||
|
or at(x - 1, y) is None or at(x + 1, y) is None
|
||||||
|
or at(x, y - 1) is None or at(x, y + 1) is None):
|
||||||
|
cv.set(x, y, col)
|
||||||
|
|
||||||
|
|
||||||
|
def tatter_bottom(cv: Canvas, seed: int, max_cut: int) -> None:
|
||||||
|
for x in range(cv.w):
|
||||||
|
cut = int(hash01(hash2(seed, x)) * (max_cut + 1))
|
||||||
|
for i in range(cut):
|
||||||
|
cv.set(x, cv.h - 1 - i, None)
|
||||||
|
|
||||||
|
|
||||||
|
def eyes(cv: Canvas, x0: int, x1: int, y: int, col) -> None:
|
||||||
|
cv.set(x0, y, col)
|
||||||
|
cv.set(x1, y, col)
|
||||||
|
|
||||||
|
|
||||||
|
# --- твари -------------------------------------------------------------------
|
||||||
|
# Фаза шага: swing — размах конечностей, bob — подскок корпуса. Обе выведены из
|
||||||
|
# одного угла, поэтому цикл замкнут и на стыке кадров не дёргается.
|
||||||
|
def phase(frame: int) -> tuple[float, float]:
|
||||||
|
a = 2.0 * math.pi * frame / FRAMES
|
||||||
|
return math.sin(a), math.cos(2.0 * a)
|
||||||
|
|
||||||
|
|
||||||
|
def shambler(frame: int, body):
|
||||||
|
swing, bob = phase(frame)
|
||||||
|
cv = Canvas(13, 17)
|
||||||
|
dy = 0.6 * bob # тяжёлый переваливающийся шаг
|
||||||
|
fill_blobs(cv, [
|
||||||
|
(6.5, 10.0 + dy, 4.2, 6.0),
|
||||||
|
(8.0 + 0.5 * swing, 4.5 + dy, 3.0, 2.8),
|
||||||
|
(2.5 - 0.9 * swing, 10.5 + dy, 1.8, 4.5),
|
||||||
|
(10.5 + 0.9 * swing, 11.5 + dy, 1.6, 4.0),
|
||||||
|
(5.0 + 0.8 * swing, 15.0, 2.4, 2.4),
|
||||||
|
], body)
|
||||||
|
shade_silhouette(cv, body)
|
||||||
|
maw = shade(body, 0.20)
|
||||||
|
for x in range(7, 10):
|
||||||
|
cv.paint(int(x + 0.5 * swing), int(5 + dy), maw)
|
||||||
|
tatter_bottom(cv, 0x5A17, 2)
|
||||||
|
outline(cv, shade(body, 0.28))
|
||||||
|
return cv
|
||||||
|
|
||||||
|
|
||||||
|
def rusher(frame: int, body):
|
||||||
|
swing, bob = phase(frame)
|
||||||
|
cv = Canvas(11, 18)
|
||||||
|
dy = 0.9 * bob # быстрый частый шаг: подскок заметнее
|
||||||
|
fill_blobs(cv, [
|
||||||
|
(5.0, 10.5 + dy, 2.6, 6.0),
|
||||||
|
(7.0 + 0.4 * swing, 3.8 + dy, 2.4, 3.2),
|
||||||
|
(8.6 + 1.1 * swing, 8.0 + dy, 1.4, 3.6),
|
||||||
|
(2.0 - 1.1 * swing, 11.0 + dy, 1.3, 3.4),
|
||||||
|
(4.5 + 1.0 * swing, 15.5, 2.2, 2.2),
|
||||||
|
], body)
|
||||||
|
shade_silhouette(cv, body)
|
||||||
|
eyes(cv, int(6 + 0.4 * swing), int(8 + 0.4 * swing), int(3 + dy), shade(body, 1.85))
|
||||||
|
tatter_bottom(cv, 0x9E37, 3)
|
||||||
|
outline(cv, shade(body, 0.30))
|
||||||
|
return cv
|
||||||
|
|
||||||
|
|
||||||
|
def spitter(frame: int, body):
|
||||||
|
swing, bob = phase(frame)
|
||||||
|
cv = Canvas(16, 14)
|
||||||
|
dy = 0.5 * bob
|
||||||
|
fill_blobs(cv, [
|
||||||
|
(8.0, 8.0 + dy, 6.6, 4.8),
|
||||||
|
(8.0, 3.6 + dy, 3.4, 2.6),
|
||||||
|
(3.0 - 0.7 * swing, 12.0, 1.8, 1.8),
|
||||||
|
(13.0 + 0.7 * swing, 12.0, 1.8, 1.8),
|
||||||
|
], body)
|
||||||
|
shade_silhouette(cv, body)
|
||||||
|
maw, bile = shade(body, 0.18), shade(body, 1.70)
|
||||||
|
for x in range(5, 12):
|
||||||
|
cv.paint(x, int(4 + dy), maw)
|
||||||
|
for x in range(6, 11):
|
||||||
|
cv.paint(x, int(5 + dy), maw)
|
||||||
|
for y in range(7, 11):
|
||||||
|
for x in range(2, cv.w - 2):
|
||||||
|
if (x * 3 + y * 7) % 13 == 0:
|
||||||
|
cv.paint(x, y, bile)
|
||||||
|
outline(cv, shade(body, 0.30))
|
||||||
|
return cv
|
||||||
|
|
||||||
|
|
||||||
|
def brute(frame: int, body):
|
||||||
|
swing, bob = phase(frame)
|
||||||
|
cv = Canvas(22, 25)
|
||||||
|
dy = 0.7 * bob # туша качается медленно и широко
|
||||||
|
fill_blobs(cv, [
|
||||||
|
(11.0, 9.0 + dy, 9.0, 6.0),
|
||||||
|
(11.0, 16.0 + dy, 5.5, 6.0),
|
||||||
|
(11.0, 4.5 + dy, 3.0, 2.8),
|
||||||
|
(2.5 - 0.8 * swing, 17.0 + dy, 2.6, 5.5),
|
||||||
|
(19.5 + 0.8 * swing, 17.0 + dy, 2.6, 5.5),
|
||||||
|
(7.0 + 1.2 * swing, 23.0, 3.0, 2.2),
|
||||||
|
(15.0 - 1.2 * swing, 23.0, 3.0, 2.2),
|
||||||
|
], body)
|
||||||
|
shade_silhouette(cv, body)
|
||||||
|
bone, dark = shade(body, 1.55), shade(body, 0.34)
|
||||||
|
for i in range(4):
|
||||||
|
cv.paint(3 + i, int(5 + i + dy), bone)
|
||||||
|
cv.paint(cv.w - 4 - i, int(5 + i + dy), bone)
|
||||||
|
for y in range(cv.h - 6, cv.h - 2):
|
||||||
|
for x in range(1, 4):
|
||||||
|
cv.paint(x, y, bone)
|
||||||
|
cv.paint(cv.w - 1 - x, y, bone)
|
||||||
|
eyes(cv, 10, 12, int(4 + dy), dark)
|
||||||
|
outline(cv, shade(body, 0.25))
|
||||||
|
return cv
|
||||||
|
|
||||||
|
|
||||||
|
def lurker(frame: int, body):
|
||||||
|
swing, bob = phase(frame)
|
||||||
|
cv = Canvas(17, 15)
|
||||||
|
dy = 0.5 * bob
|
||||||
|
# Паучий ход: лапы работают в противофазе парами, тело почти не качается.
|
||||||
|
fill_blobs(cv, [
|
||||||
|
(8.5, 9.0 + dy, 4.0, 3.6),
|
||||||
|
(8.5, 5.5 + dy, 2.4, 2.2),
|
||||||
|
(2.0 - 0.8 * swing, 7.0 - 0.9 * swing, 1.3, 4.6),
|
||||||
|
(15.0 + 0.8 * swing, 7.0 + 0.9 * swing, 1.3, 4.6),
|
||||||
|
(4.0 + 0.9 * swing, 12.5, 1.4, 2.2),
|
||||||
|
(13.0 - 0.9 * swing, 12.5, 1.4, 2.2),
|
||||||
|
], body)
|
||||||
|
shade_silhouette(cv, body)
|
||||||
|
claw = shade(body, 1.75)
|
||||||
|
cv.paint(1, 2, claw)
|
||||||
|
cv.paint(2, 3, claw)
|
||||||
|
cv.paint(cv.w - 2, 2, claw)
|
||||||
|
cv.paint(cv.w - 3, 3, claw)
|
||||||
|
eyes(cv, 7, 10, int(5 + dy), shade(body, 1.9))
|
||||||
|
outline(cv, shade(body, 0.26))
|
||||||
|
return cv
|
||||||
|
|
||||||
|
|
||||||
|
# Цвета — копия EnemyDef из src/sim/target.cpp. Разъедутся — тварь сменит цвет
|
||||||
|
# при переключении на собранные спрайты, и это будет видно сразу.
|
||||||
|
KINDS = [
|
||||||
|
("shambler", shambler, (116, 52, 52), 13, 17),
|
||||||
|
("rusher", rusher, (158, 74, 44), 11, 18),
|
||||||
|
("spitter", spitter, (96, 132, 72), 16, 14),
|
||||||
|
("brute", brute, (142, 60, 78), 22, 25),
|
||||||
|
("lurker", lurker, (88, 62, 118), 17, 15),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
for name, maker, colour, w, h in KINDS:
|
||||||
|
sheet = Image.new("RGBA", (w * FRAMES, h), (0, 0, 0, 0))
|
||||||
|
for f in range(FRAMES):
|
||||||
|
sheet.paste(maker(f, colour).image(), (f * w, 0))
|
||||||
|
path = OUT_DIR / f"{name}_walk.png"
|
||||||
|
sheet.save(path)
|
||||||
|
print(f"{path.name}: {w * FRAMES}x{h} ({FRAMES} кадров по {w}x{h})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
assets/sources/monsters/brute_attack.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
assets/sources/monsters/brute_death.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
assets/sources/monsters/brute_hit.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/sources/monsters/brute_walk.png
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
assets/sources/monsters/lurker_attack.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
assets/sources/monsters/lurker_death.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
assets/sources/monsters/lurker_hit.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
assets/sources/monsters/lurker_walk.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
assets/sources/monsters/rusher_attack.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
assets/sources/monsters/rusher_death.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
assets/sources/monsters/rusher_hit.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/sources/monsters/rusher_walk.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
assets/sources/monsters/shambler_attack.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
assets/sources/monsters/shambler_death.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
assets/sources/monsters/shambler_hit.png
Normal file
|
After Width: | Height: | Size: 73 KiB |