feat!: Half-implemented sprite container

- Updated all assets
- Refactored door again
- Half of the sprites are visually broken for now
This commit is contained in:
Alexey 2026-04-01 09:40:00 +03:00
commit b2efc73602
22 changed files with 165 additions and 31 deletions

View file

@ -24,6 +24,7 @@ pub fn load_level (
level_handle: Res<LevelAssetHandle>,
mut commands: Commands,
asset_server: Res<AssetServer>,
textures: Res<super::LayoutTextures>,
level_assets: Res<Assets<structs::LevelAsset>>,
) {
let Some(level) = level_assets.get(&level_handle.0) else {
@ -61,7 +62,7 @@ pub fn load_level (
for DoorData { pos, facing_left, lock } in level.interactive.doors.iter() {
let door_pos = vec2(meters(pos.x + 0.5), meters(pos.y));
let mut door = parent.spawn(door_bundle(&asset_server, door_pos, *facing_left));
let mut door = parent.spawn(door_bundle(&textures, door_pos, *facing_left));
if let Some(lock_facing_left) = lock {
door.with_child(padlock_bundle(&asset_server, *lock_facing_left));
}
@ -87,7 +88,7 @@ pub fn load_level (
for ContainerData { pos, size, items } in level.interactive.containers.iter() {
let pos = vec2(meters(pos.x), meters(pos.y));
let mut container = parent.spawn(container_bundle(&asset_server, pos, *size));
let mut container = parent.spawn(container_bundle(&textures, pos, *size));
for item in items {
// TODO: replace with proper item-by-id system

View file

@ -12,8 +12,6 @@ use crate::{
use super::*;
const CRATE_CLOSED_ASSET: &'static str = "sprites/interactive/crate_closed.png";
#[derive(Component, Clone, Copy, Default, Reflect, Debug, PartialEq, Eq)]
#[reflect(Component, Clone, Default, Debug, PartialEq)]
#[require(Sprite, InteractiveObject, Inventory)]
@ -38,15 +36,26 @@ pub fn on_container_interact(
}
pub fn container_bundle(
asset_server: &Res<AssetServer>,
textures: &Res<LayoutTextures>,
position: Vec2,
inventory_size: UVec2,
) -> impl Bundle {
let image = asset_server.load(CRATE_CLOSED_ASSET);
let texture_atlas = TextureAtlas {
layout: textures.container.atlas.clone(),
index: textures.container.indices["main"],
};
let hl_texture_atlas = texture_atlas.clone().with_index(textures.container.indices["highlighted"]);
let sprite = Sprite {
image: textures.container.image.clone(),
texture_atlas: Some(texture_atlas),
..default()
};
let mut highlight_sprite = sprite.clone();
highlight_sprite.texture_atlas = Some(hl_texture_atlas);
(
Container,
Transform::from_xyz(position.x, position.y - meters(0.5), 0.),
Sprite::from_image(image),
sprite,
Inventory::new(inventory_size),
Children::spawn((
Spawn((
@ -54,6 +63,11 @@ pub fn container_bundle(
Sensor,
Transform::from_xyz(0., meters(0.5), 0.),
)),
Spawn((
highlight_sprite,
Transform::from_xyz(0., 0., 1.),
Visibility::Hidden,
)),
)),
Name::new(format!("Container {}x{}", inventory_size.x, inventory_size.y)),
)

View file

@ -6,12 +6,9 @@ use crate::meters;
use super::*;
const DOOR_OPENED_ASSET: &'static str = "sprites/interactive/door_opened.png";
const DOOR_CLOSED_ASSET: &'static str = "sprites/interactive/door_closed.png";
#[derive(Component, Clone, Copy, Reflect, PartialEq, Eq, Debug)]
#[reflect(Component, Clone, Default, PartialEq, Debug)]
#[require(Sprite, InteractiveObject)]
#[require(InteractiveObject)]
pub struct Door(pub i8);
#[derive(Component, Clone, Copy, Reflect, Default, PartialEq, Eq, Debug)]
@ -19,20 +16,33 @@ pub struct Door(pub i8);
#[require(Collider, LightOccluder2d)]
pub struct DoorCollider;
#[derive(Component, Clone, Copy, Reflect, Default, PartialEq, Eq, Debug)]
#[reflect(Component, Clone, Default, PartialEq, Debug)]
#[require(Sprite)]
pub struct DoorHighlight;
impl Default for Door {
fn default() -> Self {
Self(1)
}
}
impl Door {
#[inline(always)]
pub fn is_facing_left(&self) -> bool {
self.0 < 0
}
}
pub fn on_door_interact(
event: On<InteractionEvent>,
mut commands: Commands,
locked_query: Query<(), With<Locked>>,
door_query: Query<(&Door, &Children)>,
door_collider_query: Query<Entity, With<DoorCollider>>,
mut sprite_query: Query<(&mut Sprite, &mut Transform)>,
asset_server: Res<AssetServer>,
mut sprite_query: Query<&mut Transform, With<Sprite>>,
highlight_query: Query<(), With<DoorHighlight>>,
textures: Res<LayoutTextures>,
) {
if locked_query.get(event.entity).is_ok() {
return;
@ -43,13 +53,18 @@ pub fn on_door_interact(
let maybe_door_collider = children.iter()
.find_map(|id| if let Ok(id) = door_collider_query.get(id) { Some(id) } else { None } );
for child in children {
if let Ok((mut sprite, mut transform)) = sprite_query.get_mut(*child) {
let (image, translation) = if maybe_door_collider.is_none() { (DOOR_CLOSED_ASSET, 0.) }
else { (DOOR_OPENED_ASSET, door.0 as f32 * meters(0.5)) };
sprite.image = asset_server.load(image);
transform.translation.x = translation;
break;
match sprite_query.get_mut(*child) {
Ok(mut transform) => {
let translation = if maybe_door_collider.is_none() { 0. }
else { door.0 as f32 * meters(0.5) };
transform.translation.x = translation;
},
Err(_) => continue,
}
let texture = if maybe_door_collider.is_none() { &textures.door_closed } else { &textures.door_opened };
let (sprite, highlight_sprite) = door_sprites(texture, door.is_facing_left());
let needed_sprite = if highlight_query.get(*child).is_err() { sprite } else { highlight_sprite };
commands.entity(*child).insert(needed_sprite);
}
if let Some(id) = maybe_door_collider {
@ -68,9 +83,26 @@ pub fn door_collider_bundle() -> impl Bundle {
)
}
pub fn door_bundle(asset_server: &Res<AssetServer>, position: Vec2, facing_left: bool) -> impl Bundle {
fn door_sprites(texture: &AtlasLayoutTexture, facing_left: bool) -> (Sprite, Sprite) {
let texture_atlas = TextureAtlas {
layout: texture.atlas.clone(),
index: texture.indices["main"],
};
let hl_texture_atlas = texture_atlas.clone().with_index(texture.indices["highlighted"]);
let sprite = Sprite {
image: texture.image.clone(),
flip_x: facing_left,
texture_atlas: Some(texture_atlas),
..default()
};
let mut highlight_sprite = sprite.clone();
highlight_sprite.texture_atlas = Some(hl_texture_atlas);
(sprite, highlight_sprite)
}
pub fn door_bundle(textures: &Res<LayoutTextures>, position: Vec2, facing_left: bool) -> impl Bundle {
let direction = if facing_left { -1 } else { 1 };
let image = asset_server.load(DOOR_CLOSED_ASSET);
let (sprite, highlight_sprite) = door_sprites(&textures.door_closed, facing_left);
(
Door(direction),
Transform::from_xyz(position.x, position.y, 0.),
@ -79,17 +111,18 @@ pub fn door_bundle(asset_server: &Res<AssetServer>, position: Vec2, facing_left:
door_collider_bundle(),
(
Collider::cuboid(meters(0.5), meters(1.)),
Transform::default(),
Sensor,
Transform::from_xyz(0., 0., 0.),
),
(
Sprite {
image,
flip_x: facing_left,
..default()
},
Transform::from_xyz(0., 0., 0.),
sprite,
),
(
highlight_sprite,
Transform::from_xyz(0., 0., 1.),
DoorHighlight,
Visibility::Hidden,
)
],
)
}

View file

@ -3,7 +3,7 @@ use bevy_light_2d::prelude::*;
use crate::meters;
const LAMP_IMAGE_PATH: &'static str = "sprites/interactive/lamp.png";
const LAMP_IMAGE_PATH: &'static str = "sprites/level/lamp.png";
#[derive(Component, Clone, Copy, Default, Reflect, Debug, PartialEq, Eq)]
#[reflect(Component, Clone, Default, Debug, PartialEq)]

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
use bevy::prelude::*;
pub mod asset;
@ -35,3 +37,38 @@ pub struct LevelAssetHandle(Handle<asset::structs::LevelAsset>);
pub struct InteractionEvent {
pub entity: Entity,
}
#[derive(Reflect, Clone, PartialEq, Eq, Debug, Deref, DerefMut, Default)]
#[reflect(Clone, PartialEq, Debug, Default)]
pub struct SingleLayoutTexture(Handle<Image>);
#[derive(Reflect, Clone, PartialEq, Eq, Debug, Default)]
#[reflect(Clone, PartialEq, Debug, Default)]
pub struct AtlasLayoutTexture {
image: Handle<Image>,
atlas: Handle<TextureAtlasLayout>,
indices: HashMap<String, usize>,
}
impl AtlasLayoutTexture {
#[inline(always)]
pub fn new(
image: Handle<Image>,
atlas: Handle<TextureAtlasLayout>,
indices: HashMap<String, usize>,
) -> Self {
Self { image, atlas, indices }
}
}
#[derive(Resource, Reflect, Clone, PartialEq, Eq, Debug, Default)]
#[reflect(Resource, Clone, PartialEq, Debug, Default)]
pub struct LayoutTextures {
pub container: AtlasLayoutTexture,
pub door_closed: AtlasLayoutTexture,
pub door_opened: AtlasLayoutTexture,
pub light: SingleLayoutTexture,
pub lock: AtlasLayoutTexture,
pub stairs: AtlasLayoutTexture,
pub tilemap: SingleLayoutTexture,
}

View file

@ -91,3 +91,50 @@ pub fn setup_world(
let level_handle = asset_server.load(startup_args.level.clone());
commands.insert_resource(LevelAssetHandle(level_handle));
}
fn double_atlas(
atlases: &mut ResMut<Assets<TextureAtlasLayout>>,
indices: &mut HashMap<String, usize>,
single_size: UVec2
) -> Handle<TextureAtlasLayout> {
let mut atlas = TextureAtlasLayout::new_empty(uvec2(single_size.x * 2, single_size.y));
indices.insert(
"main".to_owned(),
atlas.add_texture(URect::from_corners(
uvec2(0, 0),
uvec2(single_size.x, single_size.y)
)),
);
indices.insert(
"highlighted".to_owned(),
atlas.add_texture(URect::from_corners(
uvec2(single_size.x, 0),
uvec2(single_size.x * 2, single_size.y)
)),
);
atlases.add(atlas)
}
pub fn load_layout_textures(
asset_server: Res<AssetServer>,
mut atlases: ResMut<Assets<TextureAtlasLayout>>,
mut textures: ResMut<LayoutTextures>,
) {
// Container
let image = asset_server.load("sprites/interactive/container.png");
let mut indices = HashMap::new();
let atlas = double_atlas(&mut atlases, &mut indices, uvec2(32, 16));
textures.container = AtlasLayoutTexture::new(image, atlas, indices);
// Closed door
let image = asset_server.load("sprites/interactive/door_closed.png");
let mut indices = HashMap::new();
let atlas = double_atlas(&mut atlases, &mut indices, uvec2(4, 32));
textures.door_closed = AtlasLayoutTexture::new(image, atlas, indices);
// Opened door
let image = asset_server.load("sprites/interactive/door_opened.png");
let mut indices = HashMap::new();
let atlas = double_atlas(&mut atlases, &mut indices, uvec2(16, 32));
textures.door_opened = AtlasLayoutTexture::new(image, atlas, indices);
}

View file

@ -110,9 +110,11 @@ impl Plugin for ExpeditionPlugin {
.init_state::<LoadingState>()
.init_state::<GameState>()
.insert_resource(ui::WindowSize::default())
.insert_resource(layout::LayoutTextures::default())
.add_systems(Startup, (
setup_global,
layout::systems::setup_world,
layout::systems::load_layout_textures,
))
.add_systems(Update, (
insert_entity_name,

View file

@ -7,8 +7,8 @@ use crate::layout::asset::structs::LevelAsset;
fn deserialize_from_str() {
let level_str = include_str!("../../assets/levels/level.toml");
let level_alt_str = include_str!("../../assets/levels/level_alt.toml");
let level = toml::de::from_str::<LevelAsset>(level_str).unwrap();
let level_alt = toml::de::from_str::<LevelAsset>(level_alt_str).unwrap();
let _ = toml::de::from_str::<LevelAsset>(level_str).unwrap();
let _ = toml::de::from_str::<LevelAsset>(level_alt_str).unwrap();
}
#[test]