Конвейер ассетов: манифесты, риги, каталоги, инструменты

Всё, из чего собираются спрайты, и ничего собранного. Источник истины —
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>
This commit is contained in:
z.kirill 2026-08-13 21:06:09 +03:00
parent 2713204c00
commit 8d35f4c2ec
139 changed files with 6669 additions and 0 deletions

Binary file not shown.

View 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}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View 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

View 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]

View 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]

View 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]

View 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_}

View 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

View 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]

View 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_}

View 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]

View 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

Binary file not shown.

View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

21
assets/sources/README.md Normal file
View 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).

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 B

View 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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Some files were not shown because too many files have changed in this diff Show More