aboutsummaryrefslogtreecommitdiff
path: root/pixel-client/src
diff options
context:
space:
mode:
Diffstat (limited to 'pixel-client/src')
-rw-r--r--pixel-client/src/config.rs60
-rw-r--r--pixel-client/src/game.rs518
-rw-r--r--pixel-client/src/helper.rs33
-rw-r--r--pixel-client/src/main.rs180
-rw-r--r--pixel-client/src/menu/background.rs97
-rw-r--r--pixel-client/src/menu/credits.rs52
-rw-r--r--pixel-client/src/menu/ingame.rs78
-rw-r--r--pixel-client/src/menu/main.rs116
-rw-r--r--pixel-client/src/menu/mod.rs22
-rw-r--r--pixel-client/src/menu/settings.rs66
-rw-r--r--pixel-client/src/profiler.rs66
-rw-r--r--pixel-client/src/render/font.rs68
-rw-r--r--pixel-client/src/render/misc.rs46
-rw-r--r--pixel-client/src/render/mod.rs192
-rw-r--r--pixel-client/src/render/sprite.rs123
-rw-r--r--pixel-client/src/strings.rs45
-rw-r--r--pixel-client/src/tilemap.rs116
-rw-r--r--pixel-client/src/ui.rs273
18 files changed, 0 insertions, 2151 deletions
diff --git a/pixel-client/src/config.rs b/pixel-client/src/config.rs
deleted file mode 100644
index 1499b1cd..00000000
--- a/pixel-client/src/config.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use anyhow::{anyhow, Result};
-use serde::{Deserialize, Serialize};
-use std::{
- fs::{read_to_string, rename, File},
- io::Write,
- path::PathBuf,
-};
-
-#[derive(Serialize, Deserialize)]
-pub struct Config {
- pub username: String,
-}
-
-impl Config {
- pub fn path() -> Result<PathBuf> {
- Ok(xdg::BaseDirectories::with_prefix("pixelcurry").place_config_file("config.toml")?)
- }
- pub fn load() -> Result<Self> {
- let path = Self::path()?;
- if path.exists() {
- Ok(toml::from_str(&read_to_string(path)?)?)
- } else {
- File::create(path)?.write_all(toml::to_string(&Self::initial()?)?.as_bytes())?;
- Self::load()
- }
- }
- pub fn save(&self) -> Result<()> {
- let path = Self::path()?;
- let temp = path.with_added_extension("~");
- File::create(&temp)?.write_all(toml::to_string(self)?.as_bytes())?;
- rename(temp, path)?;
- Ok(())
- }
- pub fn initial() -> Result<Self> {
- Ok(Config {
- username: users::get_current_username()
- .ok_or(anyhow!("current user has no name"))?
- .to_str()
- .ok_or(anyhow!("current user's name is not valid UTF8"))?
- .to_owned(),
- })
- }
-}
diff --git a/pixel-client/src/game.rs b/pixel-client/src/game.rs
deleted file mode 100644
index d71b676e..00000000
--- a/pixel-client/src/game.rs
+++ /dev/null
@@ -1,518 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use crate::{
- config::Config,
- helper::InterpolateExt,
- render::{
- misc::MiscTextures,
- sprite::{Sprite, SpriteDraw},
- AtlasLayout, Renderer,
- },
- tilemap::Tilemap,
- State,
-};
-use hurrycurry_client_lib::{network::sync::Network, spatial_index::SpatialIndex, Involvement};
-use hurrycurry_protocol::{
- glam::{IVec2, Vec2},
- movement::MovementBase,
- Character, Gamedata, Hand, ItemIndex, ItemLocation, Message, MessageTimeout, PacketC, PacketS,
- PlayerClass, PlayerID, RecipeIndex, Score, TileIndex,
-};
-use log::{info, warn};
-use sdl2::{
- keyboard::{KeyboardState, Scancode},
- rect::Rect,
-};
-use std::collections::{HashMap, HashSet};
-
-pub struct Game {
- network: Network,
-
- data: Gamedata,
- tiles: HashMap<IVec2, Tile>,
- tilemap: Tilemap,
- walkable: HashSet<IVec2>,
- players: HashMap<PlayerID, Player>,
- players_spatial_index: SpatialIndex<PlayerID>,
- items_removed: Vec<Item>,
- my_id: PlayerID,
-
- camera_center: Vec2,
- misc_textures: MiscTextures,
- item_sprites: Vec<Sprite>,
- movement_send_cooldown: f32,
- interacting: bool,
- score: Score,
-}
-
-pub struct Tile {
- _kind: TileIndex,
- item: Option<Item>,
-}
-
-pub struct Player {
- movement: MovementBase,
- items: [Option<Item>; 2],
- message_persist: Option<(Message, MessageTimeout)>,
- _name: String,
- _character: Character,
- _class: PlayerClass,
- interact_target_anim: Vec2,
- interact_target_anim_pressed: f32,
-}
-
-pub struct Item {
- position: Vec2,
- parent_position: Vec2,
- kind: ItemIndex,
- alive: f32,
- active: Option<Involvement>,
-}
-
-impl Game {
- pub fn new(mut network: Network, config: &Config, layout: &AtlasLayout) -> Self {
- network.queue_out.push_back(PacketS::Join {
- id: None,
- name: config.username.clone(),
- class: PlayerClass::Chef,
- character: Character::default(),
- position: None,
- });
-
- Self {
- network,
- tiles: HashMap::new(),
- players: HashMap::new(),
- tilemap: Tilemap::default(),
- my_id: PlayerID(0),
- data: Gamedata::default(),
- walkable: HashSet::new(),
- movement_send_cooldown: 0.,
- misc_textures: MiscTextures::init(layout),
- item_sprites: Vec::new(),
- items_removed: Vec::new(),
- interacting: false,
- score: Score::default(),
- players_spatial_index: SpatialIndex::default(),
- camera_center: Vec2::ZERO,
- }
- }
-
- pub fn tick(
- &mut self,
- dt: f32,
- keyboard: &KeyboardState,
- layout: &AtlasLayout,
- ) -> Option<Box<State>> {
- if let Err(e) = self.network.poll() {
- eprintln!("network error: {e}");
- return Some(Box::new(State::Quit));
- }
-
- for packet in self.network.queue_in.drain(..).collect::<Vec<_>>() {
- self.packet_in(packet, layout);
- }
-
- let mut direction = IVec2::new(
- keyboard.is_scancode_pressed(Scancode::D) as i32
- - keyboard.is_scancode_pressed(Scancode::A) as i32,
- keyboard.is_scancode_pressed(Scancode::S) as i32
- - keyboard.is_scancode_pressed(Scancode::W) as i32,
- )
- .as_vec2();
- let boost = keyboard.is_scancode_pressed(Scancode::K);
- let interact = keyboard.is_scancode_pressed(Scancode::Space)
- | keyboard.is_scancode_pressed(Scancode::J);
-
- if interact {
- direction *= 0.;
- }
-
- self.movement_send_cooldown -= dt;
- let send_movement = self.movement_send_cooldown < 0.;
- if send_movement {
- self.movement_send_cooldown += 0.04
- }
-
- self.score.time_remaining -= dt as f64;
- self.score.time_remaining -= self.score.time_remaining.max(0.);
-
- if interact != self.interacting {
- if interact {
- self.network.queue_out.push_back(PacketS::Interact {
- player: self.my_id,
- pos: Some(self.players[&self.my_id].movement.get_interact_target()),
- hand: Hand(0),
- });
- } else {
- self.network.queue_out.push_back(PacketS::Interact {
- player: self.my_id,
- pos: None,
- hand: Hand(0),
- });
- }
- self.interacting = interact;
- }
-
- if let Some(player) = self.players.get_mut(&self.my_id) {
- player.movement.input(direction, boost);
-
- if send_movement {
- self.network
- .queue_out
- .push_back(player.movement.movement_packet_s(self.my_id));
- }
-
- player.interact_target_anim.exp_to(
- player.movement.get_interact_target().as_vec2() + Vec2::new(0., -0.4),
- dt * 20.,
- );
- player
- .interact_target_anim_pressed
- .exp_to(if interact { 1. } else { 0. }, dt * 10.);
-
- self.camera_center.exp_to(player.movement.position, dt * 5.);
- }
-
- for (&pid, player) in &mut self.players {
- player.movement.update(&self.walkable, dt);
- if let Some((_, timeout)) = &mut player.message_persist {
- timeout.remaining -= dt;
- if timeout.remaining < 0. {
- player.message_persist = None;
- }
- }
- self.players_spatial_index
- .update_entry(pid, player.movement.position);
- }
-
- self.players_spatial_index.all(|p1, pos1| {
- self.players_spatial_index.query(pos1, 2., |p2, _pos2| {
- if p1 != p2 {
- if let [Some(a), Some(b)] = self.players.get_disjoint_mut([&p1, &p2]) {
- a.movement.collide(&mut b.movement, dt)
- }
- }
- })
- });
-
- for player in self.players.values_mut() {
- for item in player.items.iter_mut().flatten() {
- item.parent_position = player.movement.position;
- item.tick(1., dt);
- }
- }
- for tile in self.tiles.values_mut() {
- if let Some(item) = &mut tile.item {
- item.tick(1., dt)
- }
- }
- self.items_removed.retain_mut(|i| {
- i.tick(0., dt);
- i.alive > 0.01
- });
-
- None
- }
-
- pub fn packet_in(&mut self, packet: PacketC, layout: &AtlasLayout) {
- match packet {
- PacketC::Joined { id } => self.my_id = id,
- PacketC::Data { data } => {
- self.tilemap.init(&data.tile_names, layout);
- self.item_sprites = data
- .item_names
- .iter()
- .map(|name| {
- Sprite::new(
- layout
- .get(&format!("{name}+a"))
- .copied()
- .unwrap_or_else(|| {
- warn!("no sprite for item {name:?}");
- Rect::new(0, 0, 32, 24)
- }),
- Vec2::new(0., 0.0),
- 0.1,
- )
- })
- .collect();
- self.data = data;
- }
- PacketC::UpdateMap {
- tile,
- kind,
- neighbors,
- } => {
- if let Some(kind) = kind {
- self.tiles.insert(
- tile,
- Tile {
- _kind: kind,
- item: None,
- },
- );
- if self.data.tile_collide[kind.0] {
- self.walkable.remove(&tile);
- } else {
- self.walkable.insert(tile);
- }
- } else {
- self.tiles.remove(&tile);
- self.walkable.remove(&tile);
- }
- self.tilemap.set(tile, kind, neighbors);
- }
- PacketC::AddPlayer {
- id,
- position,
- character,
- name,
- class,
- } => {
- info!("add player {} {name:?}", id.0);
- self.players.insert(
- id,
- Player {
- interact_target_anim: position,
- interact_target_anim_pressed: 0.,
- _class: class,
- _character: character,
- _name: name,
- message_persist: None,
- items: [const { None }; 2],
- movement: MovementBase {
- position,
- input_direction: Vec2::ZERO,
- input_boost: false,
- facing: Vec2::X,
- rotation: 0.,
- velocity: Vec2::ZERO,
- boosting: false,
- stamina: 0.,
- },
- },
- );
- }
- PacketC::RemovePlayer { id } => {
- info!("remove player {}", id.0);
- self.players_spatial_index.remove_entry(id);
- self.players.remove(&id);
- }
- PacketC::Movement {
- player,
- pos,
- rot,
- boost,
- dir,
- } => {
- if player != self.my_id {
- if let Some(p) = self.players.get_mut(&player) {
- p.movement.position = pos;
- p.movement.rotation = rot;
- p.movement.input(dir, boost);
- }
- }
- }
- PacketC::MoveItem { from, to } => {
- let mut item = self.get_item(from).unwrap().take();
- if let Some(item) = &mut item {
- item.parent_position = self.get_location_position(to);
- }
- *self.get_item(to).unwrap() = item;
- }
- PacketC::SetItem { location, item } => {
- let position = self.get_location_position(location);
- let slot = match location {
- ItemLocation::Tile(pos) => &mut self.tiles.get_mut(&pos).unwrap().item,
- ItemLocation::Player(pid, hand) => self
- .players
- .get_mut(&pid)
- .unwrap()
- .items
- .get_mut(hand.0)
- .unwrap(),
- };
- self.items_removed.extend(slot.take());
- *slot = item.map(|kind| Item {
- kind,
- parent_position: position,
- alive: 0.,
- position,
- active: None,
- })
- }
- PacketC::ClearProgress { item } => {
- if let Some(Some(item)) = self.get_item(item) {
- item.active = None;
- }
- }
- PacketC::SetProgress {
- item,
- position,
- speed,
- player,
- warn,
- } => {
- if let Some(Some(item)) = self.get_item(item) {
- item.active = Some(Involvement {
- position,
- speed,
- player,
- warn,
- recipe: RecipeIndex(0),
- });
- }
- }
- PacketC::ServerMessage { .. } => {
- // TODO
- }
- PacketC::Score(score) => {
- self.score = score;
- }
- PacketC::SetIngame { state: _, lobby: _ } => {
- // TODO
- }
- PacketC::Communicate {
- player,
- message,
- timeout: Some(timeout),
- } => {
- if let Some(player) = self.players.get_mut(&player) {
- player.message_persist = message.map(|m| (m, timeout));
- }
- }
- _ => (),
- }
- }
-
- pub fn get_item(&mut self, location: ItemLocation) -> Option<&mut Option<Item>> {
- match location {
- ItemLocation::Tile(pos) => Some(&mut self.tiles.get_mut(&pos)?.item),
- ItemLocation::Player(pid, hand) => {
- Some(self.players.get_mut(&pid)?.items.get_mut(hand.0)?)
- }
- }
- }
- pub fn get_location_position(&self, location: ItemLocation) -> Vec2 {
- match location {
- ItemLocation::Tile(pos) => pos.as_vec2() + 0.5,
- ItemLocation::Player(p, _) => self.players[&p].movement.position,
- }
- }
-
- pub fn draw(&self, ctx: &mut Renderer) {
- ctx.set_world_view(
- -self.camera_center + (ctx.size / ctx.get_world_scale() / 2.),
- ctx.size.min_element() / 32. / 10.,
- );
-
- self.tilemap.draw(ctx);
-
- if let Some(me) = self.players.get(&self.my_id) {
- ctx.draw_world(
- self.misc_textures
- .interact_target
- .at(me.interact_target_anim)
- .tint(
- 100,
- 100 + (me.interact_target_anim_pressed * 150.) as u8,
- 100 + ((1. - me.interact_target_anim_pressed) * 150.) as u8,
- ),
- )
- }
-
- for p in self.players.values() {
- p.draw(ctx, &self.item_sprites)
- }
- for tile in self.tiles.values() {
- if let Some(item) = &tile.item {
- item.draw(ctx, &self.item_sprites)
- }
- }
- for item in &self.items_removed {
- item.draw(ctx, &self.item_sprites)
- }
- }
-}
-
-impl Item {
- pub fn tick(&mut self, alive: f32, dt: f32) {
- self.position.exp_to(self.parent_position, dt * 20.);
- self.alive.exp_to(alive, dt * 20.);
- if let Some(active) = &mut self.active {
- active.position += active.speed * dt;
- }
- }
- pub fn draw(&self, ctx: &mut Renderer, item_sprites: &[Sprite]) {
- ctx.draw_world(
- item_sprites[self.kind.0]
- .at(self.position)
- .alpha(self.alive),
- );
- if let Some(Involvement { position, warn, .. }) = self.active {
- let (bg, fg) = if warn {
- ([100, 0, 0, 200], [255, 0, 0, 200])
- } else {
- ([0, 100, 0, 200], [0, 255, 0, 200])
- };
- ctx.draw_world(SpriteDraw::overlay(
- ctx.misc_textures.solid,
- self.position + Vec2::new(-0.5, -1.3),
- Vec2::new(1., 0.2),
- Some(bg),
- ));
- ctx.draw_world(SpriteDraw::overlay(
- ctx.misc_textures.solid,
- self.position + Vec2::new(-0.5, -1.3),
- Vec2::new(position, 0.2),
- Some(fg),
- ))
- }
- }
-}
-
-impl Player {
- pub fn draw(&self, ctx: &mut Renderer, item_sprites: &[Sprite]) {
- ctx.draw_world(
- match self._class {
- PlayerClass::Chef | PlayerClass::Bot => &ctx.misc_textures.chef,
- _ => &ctx.misc_textures.customer,
- }
- .at(self.movement.position),
- );
- if let Some((message, _timeout)) = &self.message_persist {
- match message {
- Message::Text(_) => (), // TODO
- Message::Item(item) => {
- ctx.draw_world(ctx.misc_textures.itembubble.at(self.movement.position));
- ctx.draw_world(
- item_sprites[item.0]
- .at(self.movement.position)
- .elevate(1.2)
- .scale(0.8),
- );
- }
- _ => (),
- }
- }
- for item in self.items.iter().flatten() {
- item.draw(ctx, item_sprites)
- }
- }
-}
diff --git a/pixel-client/src/helper.rs b/pixel-client/src/helper.rs
deleted file mode 100644
index 41ce5d9f..00000000
--- a/pixel-client/src/helper.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use hurrycurry_protocol::glam::Vec2;
-
-pub trait InterpolateExt {
- fn exp_to(&mut self, target: Self, dt: f32);
-}
-impl InterpolateExt for Vec2 {
- fn exp_to(&mut self, target: Self, dt: f32) {
- self.x = target.x + (self.x - target.x) * (-dt).exp();
- self.y = target.y + (self.y - target.y) * (-dt).exp();
- }
-}
-impl InterpolateExt for f32 {
- fn exp_to(&mut self, target: Self, dt: f32) {
- *self = target + (*self - target) * (-dt).exp();
- }
-}
diff --git a/pixel-client/src/main.rs b/pixel-client/src/main.rs
deleted file mode 100644
index d1efaedd..00000000
--- a/pixel-client/src/main.rs
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-#![feature(path_add_extension, iterator_try_collect)]
-use anyhow::{anyhow, Result};
-use clap::{Parser, Subcommand};
-use config::Config;
-use game::Game;
-use hurrycurry_client_lib::network::sync::Network;
-use hurrycurry_protocol::glam::Vec2;
-use menu::{ingame::IngameMenu, main::MainMenu};
-use profiler::ProfilerOverlay;
-use render::Renderer;
-use sdl2::{event::Event, keyboard::KeyboardState, mouse::MouseState, pixels::Color};
-use std::time::{Duration, Instant};
-use strings::set_language;
-
-pub mod config;
-pub mod game;
-pub mod helper;
-pub mod menu;
-pub mod profiler;
-pub mod render;
-pub mod strings;
-pub mod tilemap;
-pub mod ui;
-
-#[derive(Debug, Parser)]
-pub struct Args {
- #[clap(subcommand)]
- action: Option<Action>,
-}
-
-#[derive(Debug, Subcommand, Default)]
-pub enum Action {
- #[default]
- Menu,
- Join {
- #[arg(default_value = "ws://127.0.0.1/")]
- server_address: String,
- },
-}
-
-pub enum State {
- Ingame(IngameMenu),
- MainMenu(MainMenu),
- Quit,
-}
-
-fn main() -> Result<()> {
- env_logger::init_from_env("LOG");
-
- let args = Args::parse();
- let mut config = Config::load()?;
- set_language("de");
-
- rustls::crypto::ring::default_provider()
- .install_default()
- .expect("failed to initialize crypto things");
-
- let sdl_context = sdl2::init().map_err(|e| anyhow!("sdl2 init failed: {e}"))?;
-
- let video_subsystem = sdl_context
- .video()
- .map_err(|e| anyhow!("sdl2 video subsystem init failed: {e}"))?;
- let window = video_subsystem
- .window("Pixel Curry!", 1280, 720)
- .position_centered()
- .resizable()
- .build()
- .map_err(|e| anyhow!("sdl2 window creation failed: {e}"))?;
-
- let mut canvas = window
- .into_canvas()
- .accelerated()
- .present_vsync()
- .build()
- .map_err(|e| anyhow!("sdl2 canvas creation failed: {e}"))?;
-
- let texture_creator = canvas.texture_creator();
-
- let mut renderer = Renderer::init(&texture_creator);
-
- let mut state = match args.action.unwrap_or_default() {
- Action::Menu => State::MainMenu(MainMenu::new(renderer.atlas_layout())),
- Action::Join { server_address } => State::Ingame(IngameMenu::new(Game::new(
- Network::connect(&server_address)?,
- &config,
- renderer.atlas_layout(),
- ))),
- };
-
- let mut events = sdl_context
- .event_pump()
- .map_err(|e| anyhow!("sdl2 event pump: {e}"))?;
- let mut last_tick = Instant::now();
- let mut profiler = ProfilerOverlay::new();
-
- 'mainloop: loop {
- let (width, height) = canvas
- .output_size()
- .map_err(|_| anyhow!("cannot get canvas size"))?;
- renderer.size = Vec2::new(width as f32, height as f32);
-
- let keyboard = KeyboardState::new(&events);
- let mouse = MouseState::new(&events);
-
- let actual_dt = last_tick.elapsed();
- last_tick += actual_dt;
- let dt = actual_dt.min(Duration::from_secs_f32(1. / 30.));
-
- let next = match &mut state {
- State::Ingame(x) => {
- x.tick(dt.as_secs_f32(), &keyboard, &mouse, renderer.atlas_layout())
- }
- State::MainMenu(x) => {
- x.tick(dt.as_secs_f32(), &keyboard, &mouse, renderer.atlas_layout())
- }
- State::Quit => break Ok(()),
- };
- if let Some(next) = next {
- state = *next;
- }
-
- renderer.set_ui_view(4.);
- match &mut state {
- State::Ingame(x) => x.draw(&mut renderer, &mut config),
- State::MainMenu(x) => x.draw(&mut renderer, &mut config),
- State::Quit => (),
- }
-
- profiler.update(&mut renderer);
- canvas.set_draw_color(Color::BLACK);
- canvas.clear();
- renderer.submit(&mut canvas);
- canvas.present();
-
- for event in events.poll_iter() {
- match event {
- Event::Quit { .. } => break 'mainloop Ok(()),
- Event::KeyUp {
- keycode: Some(keycode),
- ..
- } => match &mut state {
- State::Ingame(g) => g.keyboard_event(keycode, false),
- State::MainMenu(g) => g.keyboard_event(keycode, false),
- _ => (),
- },
- Event::KeyDown {
- keycode: Some(keycode),
- ..
- } => match &mut state {
- State::Ingame(g) => g.keyboard_event(keycode, true),
- State::MainMenu(g) => g.keyboard_event(keycode, true),
- _ => (),
- },
- Event::TextInput { text, .. } => match &mut state {
- State::Ingame(g) => g.ui_state.text_input(text),
- State::MainMenu(g) => g.ui_state.text_input(text),
- _ => (),
- },
- _ => {}
- }
- }
- }
-}
diff --git a/pixel-client/src/menu/background.rs b/pixel-client/src/menu/background.rs
deleted file mode 100644
index 190858a6..00000000
--- a/pixel-client/src/menu/background.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use crate::{
- render::{sprite::SpriteDraw, AtlasLayout, Renderer},
- tilemap::Tilemap,
-};
-use hurrycurry_protocol::{
- glam::{IVec2, Vec2},
- TileIndex,
-};
-use rand::{random, rng, seq::IndexedRandom};
-
-pub struct MenuBackground {
- background: Vec2,
- map: Tilemap,
-}
-
-impl MenuBackground {
- pub fn new(layout: &AtlasLayout) -> Self {
- let mut map = Tilemap::default();
- map.init(
- &[
- "floor",
- "tomato-crate",
- "steak-crate",
- "table",
- "chair",
- "counter",
- "sink",
- "stove",
- ]
- .map(String::from),
- layout,
- );
- static BUCKETS: &[&[usize]] = &[&[], &[0, 0, 0, 0, 1, 2], &[3, 4, 5], &[6, 7]];
-
- for x in -10..11 {
- for y in -10..11 {
- let p = Vec2::new(x as f32, y as f32);
- let w = (-p.length() * 0.15).exp();
- let k = ((random::<f32>() * w) * BUCKETS.len() as f32) as usize;
- if let Some(ti) = BUCKETS[k.min(BUCKETS.len())].choose(&mut rng()) {
- map.set(IVec2::new(x, y), Some(TileIndex(*ti)), [None; 4])
- }
- }
- }
- Self {
- map,
- background: Vec2::ZERO,
- }
- }
-
- pub fn tick(&mut self, dt: f32) {
- self.background += Vec2::new(2., 3.) * dt;
- self.background %= 256.;
- }
- pub fn draw(&self, ctx: &mut Renderer) {
- ctx.set_world_view(
- ctx.size / ctx.get_world_scale() * Vec2::new(0.8, 0.2),
- ctx.size.max_element() / 32. / 15.,
- );
-
- for x in -1..=2 {
- for y in -1..=2 {
- ctx.draw_ui(SpriteDraw::underlay(
- ctx.misc_textures.clouds,
- Vec2::new(x as f32, y as f32) * 256. + self.background,
- Vec2::ONE * 256.,
- None,
- ));
- }
- }
- ctx.draw_ui(SpriteDraw::underlay(
- ctx.misc_textures.solid,
- Vec2::ZERO,
- ctx.ui_size,
- Some([0, 0, 0, 50]),
- ));
-
- self.map.draw(ctx);
- }
-}
diff --git a/pixel-client/src/menu/credits.rs b/pixel-client/src/menu/credits.rs
deleted file mode 100644
index abf71ffe..00000000
--- a/pixel-client/src/menu/credits.rs
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use crate::{render::sprite::SpriteDraw, strings::tr, ui::Ui};
-use hurrycurry_protocol::glam::Vec2;
-
-#[derive(Default)]
-pub struct CreditsMenu {}
-
-impl CreditsMenu {
- pub fn draw(&mut self, ui: &mut Ui) -> bool {
- ui.renderer.draw_ui(SpriteDraw::overlay(
- ui.renderer.misc_textures.solid,
- Vec2::ZERO,
- ui.renderer.ui_size,
- Some([0, 0, 0, 150]),
- ));
-
- let mut back = false;
-
- ui.horizontal(|ui| {
- ui.advance(Vec2::splat(30.));
- ui.vertical(|ui| {
- ui.advance(Vec2::splat(30.));
- ui.text("Pixel Curry!");
-
- ui.small_text(tr("c.credits.developed_by"));
-
- ui.text("metamuffin, BigBrotherNii");
-
- ui.advance(ui.get_remaining() - Vec2::Y * 30.);
- back = ui.button(80., tr("c.menu.back"));
- });
- });
-
- back
- }
-}
diff --git a/pixel-client/src/menu/ingame.rs b/pixel-client/src/menu/ingame.rs
deleted file mode 100644
index 66c77bce..00000000
--- a/pixel-client/src/menu/ingame.rs
+++ /dev/null
@@ -1,78 +0,0 @@
-use super::main::MainMenu;
-use crate::{
- config::Config,
- game::Game,
- render::{sprite::SpriteDraw, AtlasLayout, Renderer},
- strings::tr,
- ui::UiState,
- State,
-};
-use hurrycurry_protocol::glam::Vec2;
-use sdl2::{
- keyboard::{KeyboardState, Keycode},
- mouse::MouseState,
-};
-
-pub struct IngameMenu {
- game: Box<Game>,
- pub ui_state: UiState,
- overlay_shown: bool,
- next_state: Option<Box<State>>,
-}
-impl IngameMenu {
- pub fn new(game: Game) -> Self {
- Self {
- overlay_shown: false,
- game: Box::new(game),
- ui_state: UiState::default(),
- next_state: None,
- }
- }
- pub fn tick(
- &mut self,
- dt: f32,
- keyboard: &KeyboardState,
- mouse: &MouseState,
- layout: &AtlasLayout,
- ) -> Option<Box<State>> {
- self.game.tick(dt, keyboard, layout);
- self.ui_state.update(keyboard, mouse, dt);
- self.next_state.take()
- }
- pub fn keyboard_event(&mut self, keycode: Keycode, down: bool) {
- self.ui_state.keyboard_event(keycode, down);
- if down && keycode == Keycode::Escape {
- self.overlay_shown = !self.overlay_shown
- }
- }
- pub fn draw(&mut self, ctx: &mut Renderer, _config: &mut Config) {
- self.game.draw(ctx);
- if self.overlay_shown {
- let mut main_menu = false;
- ctx.draw_ui(SpriteDraw::overlay(
- ctx.misc_textures.solid,
- Vec2::ZERO,
- ctx.ui_size,
- Some([0, 0, 0, 130]),
- ));
- self.ui_state.draw(ctx, |ui| {
- ui.horizontal(|ui| {
- ui.advance(Vec2::splat(20.));
- ui.vertical(|ui| {
- ui.advance(Vec2::splat(20.));
- let w = 80.;
- main_menu |= ui.button(w, tr("c.menu.ingame.resume"));
- ui.advance(Vec2::Y * 10.);
- main_menu |= ui.button(w, tr("c.menu.ingame.main_menu"));
- if ui.button(w, tr("c.menu.ingame.quit")) {
- self.next_state = Some(Box::new(State::Quit))
- }
- });
- });
- });
- if main_menu {
- self.next_state = Some(Box::new(State::MainMenu(MainMenu::new(ctx.atlas_layout()))))
- }
- }
- }
-}
diff --git a/pixel-client/src/menu/main.rs b/pixel-client/src/menu/main.rs
deleted file mode 100644
index e50473d9..00000000
--- a/pixel-client/src/menu/main.rs
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use super::{
- background::MenuBackground, credits::CreditsMenu, ingame::IngameMenu, settings::SettingsMenu,
-};
-use crate::{
- config::Config,
- game::Game,
- render::{sprite::SpriteDraw, AtlasLayout, Renderer},
- strings::tr,
- ui::UiState,
- State,
-};
-use hurrycurry_client_lib::network::sync::Network;
-use hurrycurry_protocol::glam::Vec2;
-use sdl2::{
- keyboard::{KeyboardState, Keycode},
- mouse::MouseState,
-};
-
-pub struct MainMenu {
- background: MenuBackground,
- fade_in: f32,
- pub ui_state: UiState,
- server_address: String,
- next_state: Option<Box<State>>,
- settings: Option<SettingsMenu>,
- credits: Option<CreditsMenu>,
-}
-
-impl MainMenu {
- pub fn new(layout: &AtlasLayout) -> Self {
- Self {
- background: MenuBackground::new(layout),
- fade_in: 0.,
- server_address: String::from("ws://127.0.0.1"),
- ui_state: UiState::default(),
- next_state: None,
- settings: None,
- credits: None,
- }
- }
- pub fn tick(
- &mut self,
- dt: f32,
- keyboard: &KeyboardState,
- mouse: &MouseState,
- _layout: &AtlasLayout,
- ) -> Option<Box<State>> {
- self.fade_in = (self.fade_in + dt).min(1.);
- self.ui_state.update(keyboard, mouse, dt);
- self.background.tick(dt);
- self.next_state.take()
- }
- pub fn keyboard_event(&mut self, keycode: Keycode, down: bool) {
- self.ui_state.keyboard_event(keycode, down);
- }
- pub fn draw(&mut self, ctx: &mut Renderer, config: &mut Config) {
- self.background.draw(ctx);
-
- self.ui_state.draw(ctx, |ui| {
- if let Some(settings) = &mut self.settings {
- if settings.draw(ui, config) {
- self.settings = None;
- }
- return;
- }
- if let Some(credits) = &mut self.credits {
- if credits.draw(ui) {
- self.credits = None;
- }
- return;
- }
- if ui.button(80., tr("c.menu.play.connect")) {
- self.next_state = Some(Box::new(State::Ingame(IngameMenu::new(Game::new(
- Network::connect(&self.server_address).unwrap(),
- config,
- ui.renderer.atlas_layout(),
- )))))
- }
- ui.textedit(80., &mut self.server_address);
- if ui.button(80., tr("c.menu.settings")) {
- self.settings = Some(SettingsMenu::default())
- }
- if ui.button(80., tr("c.menu.about.credits")) {
- self.credits = Some(CreditsMenu::default())
- }
- if ui.button(80., tr("c.menu.quit")) {
- self.next_state = Some(Box::new(State::Quit));
- }
- ui.fill();
- });
-
- ctx.draw_ui(SpriteDraw::overlay(
- ctx.misc_textures.solid,
- Vec2::ZERO,
- ctx.ui_size,
- Some([0, 0, 0, 255 - (self.fade_in * 255.) as u8]),
- ));
- }
-}
diff --git a/pixel-client/src/menu/mod.rs b/pixel-client/src/menu/mod.rs
deleted file mode 100644
index ea8b6fbd..00000000
--- a/pixel-client/src/menu/mod.rs
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-pub mod background;
-pub mod credits;
-pub mod ingame;
-pub mod main;
-pub mod settings;
diff --git a/pixel-client/src/menu/settings.rs b/pixel-client/src/menu/settings.rs
deleted file mode 100644
index 27a0e3f4..00000000
--- a/pixel-client/src/menu/settings.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use crate::{config::Config, render::sprite::SpriteDraw, strings::tr, ui::Ui};
-use hurrycurry_protocol::glam::Vec2;
-use log::warn;
-
-pub struct Settings {
- pub username: String,
-}
-
-#[derive(Default)]
-pub struct SettingsMenu {}
-
-impl SettingsMenu {
- pub fn draw(&mut self, ui: &mut Ui, config: &mut Config) -> bool {
- ui.renderer.draw_ui(SpriteDraw::overlay(
- ui.renderer.misc_textures.solid,
- Vec2::ZERO,
- ui.renderer.ui_size,
- Some([0, 0, 0, 150]),
- ));
-
- let mut back = false;
-
- ui.horizontal(|ui| {
- ui.advance(Vec2::splat(20.));
- ui.vertical(|ui| {
- ui.advance(Vec2::splat(10.));
- ui.text(tr("c.menu.settings"));
-
- ui.horizontal(|ui| {
- ui.text(tr("c.settings.username"));
- ui.advance(Vec2::X * 20.);
- ui.textedit(100., &mut config.username);
- });
-
- ui.advance(ui.get_remaining() - Vec2::Y * 30.);
-
- if ui.button(80., tr("c.menu.back")) {
- if let Err(e) = config.save() {
- warn!("cannot save config: {e}");
- } else {
- back = true
- }
- }
- });
- });
-
- back
- }
-}
diff --git a/pixel-client/src/profiler.rs b/pixel-client/src/profiler.rs
deleted file mode 100644
index 660c06cd..00000000
--- a/pixel-client/src/profiler.rs
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use crate::render::{sprite::SpriteDraw, Renderer};
-use hurrycurry_protocol::glam::Vec2;
-use std::time::Instant;
-
-pub struct ProfilerOverlay {
- frames: usize,
- fps_timer_start: Instant,
- fps: f32,
-}
-
-impl Default for ProfilerOverlay {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl ProfilerOverlay {
- pub fn new() -> Self {
- Self {
- fps: 0.,
- fps_timer_start: Instant::now(),
- frames: 0,
- }
- }
- pub fn update(&mut self, renderer: &mut Renderer) {
- self.frames += 1;
-
- let t = self.fps_timer_start.elapsed();
-
- if t.as_secs_f32() > 0.2 {
- self.fps = self.frames as f32 / t.as_secs_f32();
- self.frames = 0;
- self.fps_timer_start += t;
- }
- let size = renderer.draw_text(
- Vec2::ZERO,
- &format!("FPS: {:.0}\nSprites: {}", self.fps, renderer.num_sprites()),
- 0.3,
- Some([255, 150, 255, 255]),
- );
- renderer.draw_ui(SpriteDraw::screen(
- renderer.misc_textures.solid,
- i32::MAX - 1,
- Vec2::ZERO,
- size,
- Some([0, 0, 0, 200]),
- ))
- }
-}
diff --git a/pixel-client/src/render/font.rs b/pixel-client/src/render/font.rs
deleted file mode 100644
index 60d27083..00000000
--- a/pixel-client/src/render/font.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use super::{sprite::SpriteDraw, AtlasLayout, Renderer};
-use hurrycurry_protocol::glam::Vec2;
-use sdl2::rect::Rect;
-
-pub struct FontTextures {
- pub glyphs: [Rect; 128],
-}
-
-impl FontTextures {
- pub fn init(layout: &AtlasLayout) -> Self {
- FontTextures {
- glyphs: (0..128)
- .map(|n| {
- layout
- .get(&format!("letter_{n}+a"))
- .copied()
- .unwrap_or(Rect::new(0, 0, 0, 0))
- })
- .collect::<Vec<_>>()
- .try_into()
- .expect("some letters are missing in the font"),
- }
- }
-}
-
-impl Renderer<'_> {
- pub fn draw_text(
- &mut self,
- position: Vec2,
- text: &str,
- scale: f32,
- tint: Option<[u8; 4]>,
- ) -> Vec2 {
- let mut cursor = position;
- let mut line_height = 0f32;
- for c in text.chars() {
- if c == '\n' {
- cursor.y += line_height;
- cursor.x = position.x
- }
- if (c as u32) < 128 {
- let r = self.font_textures.glyphs[c as usize];
- let size = Vec2::new(r.width() as f32, r.height() as f32) * scale;
- self.draw_ui(SpriteDraw::overlay(r, cursor, size, tint));
- cursor.x += size.x;
- line_height = line_height.max(size.y)
- }
- }
- (cursor - position.y) + Vec2::Y * line_height
- }
-}
diff --git a/pixel-client/src/render/misc.rs b/pixel-client/src/render/misc.rs
deleted file mode 100644
index c9f25462..00000000
--- a/pixel-client/src/render/misc.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use super::{sprite::Sprite, AtlasLayout};
-use hurrycurry_protocol::glam::Vec2;
-use sdl2::rect::Rect;
-
-pub struct MiscTextures {
- pub chef: Sprite,
- pub customer: Sprite,
- pub interact_target: Sprite,
- pub solid: Rect,
- pub clouds: Rect,
- pub itembubble: Sprite,
-}
-
-impl MiscTextures {
- pub fn init(layout: &AtlasLayout) -> Self {
- MiscTextures {
- chef: Sprite::new(*layout.get("chef+a").unwrap(), Vec2::Y * 0.3, 0.5 + 0.3),
- customer: Sprite::new(*layout.get("customer+a").unwrap(), Vec2::Y * 0.3, 0.5 + 0.3),
- interact_target: Sprite::new(
- *layout.get("interact-target-thick+a").unwrap(),
- Vec2::new(0.5, 1.0),
- 10.,
- ),
- solid: *layout.get("solid+a").unwrap(),
- clouds: *layout.get("clouds+a").unwrap(),
- itembubble: Sprite::new(*layout.get("itembubble+a").unwrap(), Vec2::Y * -1., 1.),
- }
- }
-}
diff --git a/pixel-client/src/render/mod.rs b/pixel-client/src/render/mod.rs
deleted file mode 100644
index 008e015d..00000000
--- a/pixel-client/src/render/mod.rs
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-pub mod font;
-pub mod misc;
-pub mod sprite;
-
-use font::FontTextures;
-use hurrycurry_protocol::glam::Vec2;
-use misc::MiscTextures;
-use sdl2::{
- pixels::PixelFormatEnum,
- rect::{FRect, Rect},
- render::{BlendMode, Canvas, Texture, TextureAccess, TextureCreator},
- video::{Window, WindowContext},
-};
-use sprite::SpriteDraw;
-use std::collections::HashMap;
-
-pub struct Renderer<'a> {
- metadata: AtlasLayout,
-
- font_textures: FontTextures,
- pub misc_textures: MiscTextures,
-
- pub size: Vec2,
- pub ui_size: Vec2,
- texture: Texture<'a>,
-
- world_scale: Vec2,
- world_offset: Vec2,
- pub ui_scale: Vec2,
-
- sprites: Vec<SpriteDraw>,
-}
-
-pub type AtlasLayout = HashMap<String, Rect>;
-
-impl<'a> Renderer<'a> {
- pub fn init(texture_creator: &'a TextureCreator<WindowContext>) -> Self {
- let palette = include_str!("../../assets/palette.csv")
- .split('\n')
- .filter(|l| !l.is_empty())
- .map(|s| {
- let mut toks = s.split(",");
- (
- toks.next().unwrap().chars().next().unwrap(),
- [
- toks.next().unwrap().parse::<u8>().unwrap(),
- toks.next().unwrap().parse::<u8>().unwrap(),
- toks.next().unwrap().parse::<u8>().unwrap(),
- toks.next().unwrap().parse::<u8>().unwrap(),
- ],
- )
- })
- .collect::<HashMap<_, _>>();
-
- let mut texels = vec![255; 1024 * 1024 * 4];
-
- for (y, line) in include_str!("../../assets/atlas.ta").lines().enumerate() {
- if line.is_empty() {
- continue;
- }
- for (x, char) in line.chars().enumerate() {
- let color = palette.get(&char).unwrap();
- texels[(y * 1024 + x) * 4] = color[3];
- texels[(y * 1024 + x) * 4 + 1] = color[2];
- texels[(y * 1024 + x) * 4 + 2] = color[1];
- texels[(y * 1024 + x) * 4 + 3] = color[0];
- }
- }
-
- let mut texture = texture_creator
- .create_texture(
- Some(PixelFormatEnum::RGBA8888),
- TextureAccess::Streaming,
- 1024,
- 1024,
- )
- .unwrap();
-
- texture.update(None, &texels, 1024 * 4).unwrap();
- texture.set_blend_mode(BlendMode::Blend);
-
- let atlas_layout = include_str!("../../assets/atlas.meta.csv")
- .lines()
- .filter(|l| !l.is_empty())
- .map(|l| {
- let mut toks = l.split(",");
- let x: i32 = toks.next().unwrap().parse().unwrap();
- let y: i32 = toks.next().unwrap().parse().unwrap();
- let w: u32 = toks.next().unwrap().parse().unwrap();
- let h: u32 = toks.next().unwrap().parse().unwrap();
- let name = toks.next().unwrap().to_string();
- (name, Rect::new(x, y, w, h))
- })
- .collect::<HashMap<_, _>>();
-
- Self {
- ui_scale: Vec2::ZERO,
- ui_size: Vec2::ZERO,
- misc_textures: MiscTextures::init(&atlas_layout),
- texture,
- font_textures: FontTextures::init(&atlas_layout),
- size: Vec2::ONE,
- metadata: atlas_layout,
- sprites: vec![],
- world_offset: Vec2::ZERO,
- world_scale: Vec2::ZERO,
- }
- }
-
- pub fn set_world_view(&mut self, offset: Vec2, scale: f32) {
- self.world_offset = offset;
- self.world_scale = Vec2::new(32., 24.) * scale;
- }
- pub fn set_ui_view(&mut self, scale: f32) {
- self.ui_scale = Vec2::splat(scale);
- self.ui_size = self.size / self.ui_scale;
- }
- pub fn get_world_scale(&self) -> Vec2 {
- self.world_scale
- }
-
- #[inline]
- pub fn atlas_layout(&self) -> &HashMap<String, Rect> {
- &self.metadata
- }
-
- pub fn set_modulation(&mut self, r: u8, g: u8, b: u8, a: u8) {
- self.texture.set_alpha_mod(a);
- self.texture.set_color_mod(r, g, b);
- }
- pub fn reset_modulation(&mut self) {
- self.set_modulation(255, 255, 255, 255)
- }
-
- pub fn draw_world(&mut self, sprite: SpriteDraw) {
- self.sprites.push(SpriteDraw {
- tint: sprite.tint,
- z_order: sprite.z_order,
- src: sprite.src,
- dst: FRect::new(
- (sprite.dst.x + self.world_offset.x) * self.world_scale.x,
- (sprite.dst.y + self.world_offset.y) * self.world_scale.y,
- sprite.dst.w * self.world_scale.x,
- sprite.dst.h * self.world_scale.y,
- ),
- })
- }
-
- pub fn draw_ui(&mut self, sprite: SpriteDraw) {
- self.sprites.push(SpriteDraw {
- tint: sprite.tint,
- z_order: sprite.z_order,
- src: sprite.src,
- dst: FRect::new(
- sprite.dst.x * self.ui_scale.x,
- sprite.dst.y * self.ui_scale.y,
- sprite.dst.w * self.ui_scale.x,
- sprite.dst.h * self.ui_scale.y,
- ),
- })
- }
-
- pub fn submit(&mut self, canvas: &mut Canvas<Window>) {
- self.sprites.sort();
- for SpriteDraw { src, dst, tint, .. } in self.sprites.drain(..) {
- self.texture.set_color_mod(tint[0], tint[1], tint[2]);
- self.texture.set_alpha_mod(tint[3]);
- canvas.copy_f(&self.texture, src, dst).unwrap();
- }
- }
-
- pub fn num_sprites(&self) -> usize {
- self.sprites.len()
- }
-}
diff --git a/pixel-client/src/render/sprite.rs b/pixel-client/src/render/sprite.rs
deleted file mode 100644
index ae06165a..00000000
--- a/pixel-client/src/render/sprite.rs
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use hurrycurry_protocol::glam::Vec2;
-use sdl2::rect::{FRect, Rect};
-
-pub struct Sprite {
- z_offset: f32,
- src: Rect,
- relative_dst: FRect,
-}
-
-impl Sprite {
- pub fn new(src: Rect, anchor: Vec2, elevation: f32) -> Self {
- let relative_dst = FRect::new(
- anchor.x - (src.w as f32) / 32. / 2.,
- anchor.y - (src.h as f32) / 24.,
- (src.w as f32) / 32.,
- (src.h as f32) / 24.,
- );
- Self {
- z_offset: elevation,
- src,
- relative_dst,
- }
- }
- pub fn new_tile(src: Rect) -> Self {
- Self::new(src, Vec2::new(0.5, 1.0), 0.5)
- }
- pub fn at(&self, pos: Vec2) -> SpriteDraw {
- SpriteDraw {
- z_order: ((self.z_offset + pos.y) * 24.) as i32,
- src: self.src,
- dst: FRect::new(
- self.relative_dst.x + pos.x,
- self.relative_dst.y + pos.y,
- self.relative_dst.w,
- self.relative_dst.h,
- ),
- tint: [0xff; 4],
- }
- }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct SpriteDraw {
- pub tint: [u8; 4],
- pub z_order: i32,
- pub src: Rect,
- pub dst: FRect,
-}
-
-impl SpriteDraw {
- pub fn screen(src: Rect, z_order: i32, pos: Vec2, size: Vec2, tint: Option<[u8; 4]>) -> Self {
- Self {
- dst: FRect::new(pos.x, pos.y, size.x, size.y),
- src,
- tint: tint.unwrap_or([0xff; 4]),
- z_order,
- }
- }
- pub fn overlay(src: Rect, pos: Vec2, size: Vec2, tint: Option<[u8; 4]>) -> Self {
- SpriteDraw::screen(src, i32::MAX, pos, size, tint)
- }
- pub fn underlay(src: Rect, pos: Vec2, size: Vec2, tint: Option<[u8; 4]>) -> Self {
- SpriteDraw::screen(src, i32::MIN, pos, size, tint)
- }
- pub fn alpha(mut self, alpha: f32) -> Self {
- self.tint[3] = (alpha.clamp(0., 1.) * 255.) as u8;
- self
- }
- pub fn tint(mut self, r: u8, g: u8, b: u8) -> Self {
- self.tint[0] = r;
- self.tint[1] = g;
- self.tint[2] = b;
- self
- }
- pub fn elevate(mut self, offset: f32) -> SpriteDraw {
- self.z_order += (offset * 24.) as i32;
- self.dst.set_y(self.dst.y() - offset);
- self
- }
- pub fn scale(mut self, factor: f32) -> SpriteDraw {
- self.dst
- .set_x(self.dst.x() + self.dst.width() * 0.5 * (1. - factor));
- self.dst
- .set_y(self.dst.y() + self.dst.height() * 0.5 * (1. - factor));
- self.dst.set_width(self.dst.width() * factor);
- self.dst.set_height(self.dst.height() * factor);
- self
- }
-}
-
-impl Ord for SpriteDraw {
- fn cmp(&self, other: &Self) -> std::cmp::Ordering {
- self.z_order.cmp(&other.z_order)
- }
-}
-impl PartialOrd for SpriteDraw {
- fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- Some(self.cmp(other))
- }
-}
-impl Eq for SpriteDraw {}
-impl PartialEq for SpriteDraw {
- fn eq(&self, other: &Self) -> bool {
- self.z_order == other.z_order && self.src == other.src && self.dst == other.dst
- }
-}
diff --git a/pixel-client/src/strings.rs b/pixel-client/src/strings.rs
deleted file mode 100644
index a941ecec..00000000
--- a/pixel-client/src/strings.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-use anyhow::{anyhow, Result};
-use std::{
- collections::HashMap,
- fs::read_to_string,
- ops::Index,
- path::Path,
- sync::{LazyLock, Mutex},
-};
-
-pub struct Strings(HashMap<String, String>);
-impl Index<&'static str> for Strings {
- type Output = str;
- fn index(&self, index: &'static str) -> &Self::Output {
- self.0.get(index).map(|s| s.as_str()).unwrap_or(index)
- }
-}
-
-impl Strings {
- pub fn load(path: &Path) -> Result<Self> {
- Ok(Self(
- read_to_string(path)?
- .lines()
- .skip(1)
- .map(|l| {
- let (k, v) = l.split_once("=").ok_or(anyhow!("'=' missing"))?;
- Ok::<_, anyhow::Error>((
- k.trim_end().to_owned(),
- v.trim_start().replace("%n", "\n"),
- ))
- })
- .try_collect()?,
- ))
- }
-}
-
-static TR_LOAD: Mutex<Option<Strings>> = Mutex::new(None);
-static TR: LazyLock<Strings> = LazyLock::new(|| TR_LOAD.lock().unwrap().take().unwrap());
-
-pub fn tr<'a>(s: &'static str) -> &'a str {
- &TR[s]
-}
-pub fn set_language(lang: &str) {
- *TR_LOAD.lock().unwrap() =
- Some(Strings::load(Path::new(&format!("locale/{lang}.ini"))).unwrap());
-}
diff --git a/pixel-client/src/tilemap.rs b/pixel-client/src/tilemap.rs
deleted file mode 100644
index f79d4cad..00000000
--- a/pixel-client/src/tilemap.rs
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use crate::render::{
- sprite::{Sprite, SpriteDraw},
- Renderer,
-};
-use hurrycurry_protocol::{glam::IVec2, TileIndex};
-use log::warn;
-use sdl2::rect::Rect;
-use std::collections::{HashMap, HashSet};
-
-#[derive(Default, Debug)]
-pub struct Tilemap {
- connect_group_by_tile: Vec<Option<usize>>,
- connect_members_by_group: Vec<HashSet<Option<TileIndex>>>,
- tile_srcs: Vec<[Rect; 16]>,
- tiles: HashMap<IVec2, SpriteDraw>,
-}
-
-impl Tilemap {
- pub fn init(&mut self, tile_names: &[String], sprite_rects: &HashMap<String, Rect>) {
- let tile_index = tile_names
- .iter()
- .enumerate()
- .map(|(t, i)| (i.to_string(), t))
- .collect::<HashMap<_, _>>();
- self.connect_group_by_tile = vec![None; tile_names.len()];
- self.connect_members_by_group = include_str!("../assets/connect.csv")
- .lines()
- .enumerate()
- .map(|(gid, line)| {
- line.split(",")
- .flat_map(|tile| tile_index.get(tile).copied())
- .map(|ti| {
- self.connect_group_by_tile[ti] = Some(gid);
- Some(TileIndex(ti))
- })
- .collect::<HashSet<_>>()
- })
- .collect::<Vec<_>>();
-
- self.tile_srcs = tile_names
- .iter()
- .map(|name| {
- let fallback = sprite_rects
- .get(&format!("{name}+a"))
- .copied()
- .unwrap_or_else(|| {
- warn!("no sprite for tile {name:?}");
- Rect::new(0, 0, 0, 0)
- });
-
- [
- sprite_rects.get(&format!("{name}+")),
- sprite_rects.get(&format!("{name}+w")),
- sprite_rects.get(&format!("{name}+e")),
- sprite_rects.get(&format!("{name}+we")),
- sprite_rects.get(&format!("{name}+n")),
- sprite_rects.get(&format!("{name}+wn")),
- sprite_rects.get(&format!("{name}+en")),
- sprite_rects.get(&format!("{name}+wen")),
- sprite_rects.get(&format!("{name}+s")),
- sprite_rects.get(&format!("{name}+ws")),
- sprite_rects.get(&format!("{name}+es")),
- sprite_rects.get(&format!("{name}+wes")),
- sprite_rects.get(&format!("{name}+ns")),
- sprite_rects.get(&format!("{name}+wns")),
- sprite_rects.get(&format!("{name}+ens")),
- sprite_rects.get(&format!("{name}+wens")),
- ]
- .map(|e| e.copied().unwrap_or(fallback))
- })
- .collect();
- }
-
- pub fn set(&mut self, pos: IVec2, tile: Option<TileIndex>, neighbors: [Option<TileIndex>; 4]) {
- let Some(tile) = tile else {
- self.tiles.remove(&pos);
- return;
- };
-
- let mut idx = 0;
- if let Some(gid) = self.connect_group_by_tile[tile.0] {
- let cgroup = &self.connect_members_by_group[gid];
- idx |= 0b0100 * (cgroup.contains(&neighbors[0])) as usize;
- idx |= (cgroup.contains(&neighbors[1])) as usize;
- idx |= 0b1000 * (cgroup.contains(&neighbors[2])) as usize;
- idx |= 0b0010 * (cgroup.contains(&neighbors[3])) as usize;
- }
-
- let src = self.tile_srcs[tile.0][idx];
- self.tiles
- .insert(pos, Sprite::new_tile(src).at(pos.as_vec2()));
- }
-
- pub fn draw(&self, ctx: &mut Renderer) {
- for &sprite in self.tiles.values() {
- ctx.draw_world(sprite);
- }
- }
-}
diff --git a/pixel-client/src/ui.rs b/pixel-client/src/ui.rs
deleted file mode 100644
index caa82707..00000000
--- a/pixel-client/src/ui.rs
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- Hurry Curry! - a game about cooking
- Copyright (C) 2025 Hurry Curry! Contributors
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, version 3 of the License only.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-*/
-use crate::render::{sprite::SpriteDraw, Renderer};
-use hurrycurry_protocol::glam::{IVec2, Vec2};
-use sdl2::{
- keyboard::{KeyboardState, Keycode, Scancode},
- mouse::MouseState,
-};
-
-#[derive(Default)]
-pub struct FocusDevice {
- focus: usize,
- pressing: Option<usize>,
- interact_just_pressed: bool,
- interact_just_released: bool,
- interact_down: bool,
-}
-
-#[derive(Default)]
-pub struct UiState {
- mouse_position: Vec2,
- ui_scale: Vec2,
-
- backspace: bool,
- text_input: String,
-
- keyboard_focus: FocusDevice,
- mouse_focus: FocusDevice,
-}
-
-pub struct Ui<'a, 'b> {
- cursor: Vec2,
- size: Vec2,
- cross_height: f32,
- index: usize,
- direction_horizontal: bool,
- pub renderer: &'a mut Renderer<'b>,
- state: &'a mut UiState,
-}
-
-impl UiState {
- pub fn update(&mut self, keyboard: &KeyboardState, mouse: &MouseState, _dt: f32) {
- self.mouse_position = IVec2::new(mouse.x(), mouse.y()).as_vec2() / self.ui_scale;
-
- self.mouse_focus.update(mouse.left());
- self.keyboard_focus
- .update(keyboard.is_scancode_pressed(Scancode::Space));
- }
- pub fn text_input(&mut self, text: String) {
- self.text_input = text;
- }
- pub fn keyboard_event(&mut self, keycode: Keycode, down: bool) {
- if down {
- match keycode {
- Keycode::DOWN => self.keyboard_focus.focus += 1,
- Keycode::UP if self.keyboard_focus.focus > 0 => self.keyboard_focus.focus -= 1,
- Keycode::BACKSPACE => self.backspace = true,
- _ => (),
- }
- }
- }
-
- pub fn draw(&mut self, renderer: &mut Renderer, ui: impl FnOnce(&mut Ui)) {
- self.ui_scale = renderer.ui_scale;
- self.mouse_focus.focus = usize::MAX;
- let mut u = Ui {
- cursor: Vec2::ZERO,
- direction_horizontal: false,
- size: renderer.ui_size,
- renderer,
- state: self,
- cross_height: 0.,
- index: 0,
- };
- ui(&mut u);
- self.end()
- }
- fn end(&mut self) {
- if self.mouse_focus.interact_just_released {
- self.mouse_focus.pressing = None;
- }
- if self.keyboard_focus.interact_just_released {
- self.keyboard_focus.pressing = None;
- }
- self.text_input.clear();
- self.backspace = false;
- }
-}
-
-impl FocusDevice {
- pub fn update(&mut self, interact: bool) {
- self.interact_just_pressed = interact && !self.interact_down;
- self.interact_just_released = !interact && self.interact_down;
- self.interact_down = interact;
- }
- pub fn element(&mut self, index: usize) -> (bool, bool, bool) {
- let focus = self.focus == index;
- if focus && self.interact_just_pressed {
- self.pressing = Some(index)
- };
- let pressing = self.pressing == Some(index);
- let released = self.interact_just_released && pressing && focus;
- (focus, pressing, released)
- }
-}
-
-impl Ui<'_, '_> {
- pub fn vertical(&mut self, content: impl FnOnce(&mut Ui)) {
- self.flow(false, content)
- }
- pub fn horizontal(&mut self, content: impl FnOnce(&mut Ui)) {
- self.flow(true, content)
- }
- pub fn flow(&mut self, dir: bool, content: impl FnOnce(&mut Ui)) {
- let d = self.direction_horizontal;
- let ch = self.cross_height;
- let c = self.cursor;
- self.direction_horizontal = dir;
- self.cross_height = 0.;
- content(self);
- let size = (self.cursor - c).max(if dir { Vec2::Y } else { Vec2::X } * self.cross_height);
- self.direction_horizontal = d;
- self.cross_height = ch;
- self.cursor = c;
- self.advance(size);
- }
-
- pub fn text(&mut self, text: &str) {
- self.scaled_text(text, 1.)
- }
- pub fn small_text(&mut self, text: &str) {
- self.scaled_text(text, 0.5)
- }
- pub fn scaled_text(&mut self, text: &str, scale: f32) {
- let margin = Vec2::splat(2.);
- let size = margin
- + self
- .renderer
- .draw_text(self.cursor + margin, text, scale, None)
- + margin;
- self.advance(size);
- }
- pub fn button(&mut self, w: f32, label: &str) -> bool {
- let c = self.cursor;
- let margin = Vec2::splat(4.);
- let text_size = self
- .renderer
- .draw_text(self.cursor + margin, label, 1., None);
- let size = margin + Vec2::new(w, text_size.y) + margin;
-
- self.index += 1;
-
- let mouse_rel = self.state.mouse_position - c;
- if mouse_rel.x >= 0. && mouse_rel.y >= 0. && mouse_rel.x < size.x && mouse_rel.y < size.y {
- self.state.mouse_focus.focus = self.index;
- }
-
- let (focus, pressing, released) = {
- let (mfocus, mpressing, mreleased) = self.state.mouse_focus.element(self.index);
- let (kfocus, kpressing, kreleased) = self.state.keyboard_focus.element(self.index);
- (
- mfocus || kfocus,
- mpressing || kpressing,
- mreleased || kreleased,
- )
- };
-
- let l = if pressing {
- 100
- } else if focus {
- 50
- } else {
- 30
- };
- self.renderer.draw_ui(SpriteDraw::screen(
- self.renderer.misc_textures.solid,
- i32::MAX - 1,
- c,
- size,
- Some([l, l, l, 200]),
- ));
-
- self.advance(size);
- released
- }
-
- pub fn textedit(&mut self, w: f32, content: &mut String) {
- let c = self.cursor;
- let margin = Vec2::splat(4.);
- let text_size = self
- .renderer
- .draw_text(self.cursor + margin, content, 1., None);
- let size = margin + Vec2::new(w, text_size.y) + margin;
-
- self.index += 1;
-
- let mouse_rel = self.state.mouse_position - c;
- if mouse_rel.x >= 0. && mouse_rel.y >= 0. && mouse_rel.x < size.x && mouse_rel.y < size.y {
- self.state.mouse_focus.focus = self.index;
- }
-
- if self.state.mouse_focus.interact_just_pressed
- && self.state.mouse_focus.focus == self.index
- {
- self.state.keyboard_focus.focus = self.index;
- }
-
- let keyboard_focus = self.state.keyboard_focus.focus == self.index;
-
- if keyboard_focus {
- *content += &self.state.text_input;
- self.state.text_input.clear();
- if self.state.backspace {
- content.pop();
- }
- }
-
- let focus = self.state.mouse_focus.focus == self.index || keyboard_focus;
- let l = if focus { 50 } else { 30 };
- self.renderer.draw_ui(SpriteDraw::screen(
- self.renderer.misc_textures.solid,
- i32::MAX - 1,
- c,
- size,
- Some([l, l, l, 200]),
- ));
-
- self.advance(size);
- }
-
- pub fn fill(&mut self) {
- self.renderer.draw_ui(SpriteDraw::screen(
- self.renderer.misc_textures.solid,
- i32::MAX - 1,
- self.cursor,
- self.get_remaining(),
- Some([30, 30, 30, 200]),
- ));
- }
-
- pub fn get_remaining(&self) -> Vec2 {
- if self.direction_horizontal {
- Vec2::new(self.size.x - self.cursor.x, self.cross_height)
- } else {
- Vec2::new(self.cross_height, self.size.y - self.cursor.y)
- }
- }
-
- pub fn advance(&mut self, size: Vec2) {
- if self.direction_horizontal {
- self.cursor.x += size.x;
- self.cross_height = self.cross_height.max(size.y);
- } else {
- self.cursor.y += size.y;
- self.cross_height = self.cross_height.max(size.x);
- }
- }
-}