commit fb87c0639c48020c836fa9c270b87cdd4a901457 Author: Gabriel Kaszewski Date: Mon Feb 9 15:13:50 2026 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..23e23f5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,18 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cli" +version = "0.1.0" +dependencies = [ + "game-core", +] + +[[package]] +name = "game-core" +version = "0.1.0" + +[[package]] +name = "web" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cabaf94 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "3" +members = ["game-core", "platforms/cli", "platforms/web"] diff --git a/game-core/.gitignore b/game-core/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/game-core/.gitignore @@ -0,0 +1 @@ +/target diff --git a/game-core/Cargo.toml b/game-core/Cargo.toml new file mode 100644 index 0000000..eed3145 --- /dev/null +++ b/game-core/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "game-core" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/game-core/src/assets.rs b/game-core/src/assets.rs new file mode 100644 index 0000000..14bde3d --- /dev/null +++ b/game-core/src/assets.rs @@ -0,0 +1,5 @@ +pub trait AssetLoader { + fn load_chunk(&mut self, offset: u32, length: usize, dest: &mut [u8]) -> Result; + + fn load_map(&mut self, map_id: u8, buffer: &mut [u8]) -> Result; +} diff --git a/game-core/src/combat.rs b/game-core/src/combat.rs new file mode 100644 index 0000000..934ec96 --- /dev/null +++ b/game-core/src/combat.rs @@ -0,0 +1,13 @@ +use crate::{math::Rng, types::Stats}; + +pub fn resolve_attack(rng: &mut Rng, attacker: &Stats, defender: &Stats) -> (bool, i8) { + let roll = rng.roll_d20(); + + // simplified logic for now + if roll > 10 { + let dmg = (attacker.strength / 2) as i8; + (true, dmg) + } else { + (false, 0) + } +} diff --git a/game-core/src/errors.rs b/game-core/src/errors.rs new file mode 100644 index 0000000..e69de29 diff --git a/game-core/src/input.rs b/game-core/src/input.rs new file mode 100644 index 0000000..6b729dc --- /dev/null +++ b/game-core/src/input.rs @@ -0,0 +1,8 @@ +use crate::types::Direction; + +pub enum InputEvent { + None, + Move(Direction), + Action, + Char(char), +} diff --git a/game-core/src/lib.rs b/game-core/src/lib.rs new file mode 100644 index 0000000..ceb0015 --- /dev/null +++ b/game-core/src/lib.rs @@ -0,0 +1,12 @@ +#![no_std] +extern crate alloc; + +pub mod assets; +pub mod combat; +pub mod errors; +pub mod input; +pub mod map; +pub mod math; +pub mod render; +pub mod state; +pub mod types; diff --git a/game-core/src/map.rs b/game-core/src/map.rs new file mode 100644 index 0000000..c8bdcf2 --- /dev/null +++ b/game-core/src/map.rs @@ -0,0 +1,43 @@ +use crate::types::{Direction, TileType}; + +pub struct MapView<'a> { + pub width: u8, + pub height: u8, + pub data: &'a [u8], +} + +impl<'a> MapView<'a> { + pub fn new(raw_data: &'a [u8]) -> Self { + let width = raw_data[0]; + let height = raw_data[1]; + let data = &raw_data[2..]; + MapView { + width, + height, + data, + } + } + + pub fn get_tile(&self, index: usize) -> TileType { + if index < self.data.len() { + self.data[index].into() + } else { + TileType::Void + } + } + + pub fn get_neighbor_index(&self, current_index: usize, direction: Direction) -> Option { + let x = (current_index as u8) % self.width; + let y = (current_index as u8) / self.width; + + let (nx, ny) = match direction { + Direction::North if y > 0 => (x, y - 1), + Direction::South if y < (self.height - 1) => (x, y + 1), + Direction::East if x < (self.width - 1) => (x + 1, y), + Direction::West if x > 0 => (x - 1, y), + _ => return None, + }; + + Some((ny * self.width + nx) as usize) + } +} diff --git a/game-core/src/math.rs b/game-core/src/math.rs new file mode 100644 index 0000000..1085937 --- /dev/null +++ b/game-core/src/math.rs @@ -0,0 +1,27 @@ +pub struct Rng { + state: u32, +} + +impl Rng { + pub fn new(seed: u32) -> Self { + Rng { state: seed } + } + + pub fn next(&mut self) -> u32 { + let mut x = self.state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + self.state = x; + x + } + + pub fn next_range(&mut self, min: u32, max: u32) -> u32 { + let range = max - min; + (self.next() % range) + min + } + + pub fn roll_d20(&mut self) -> u8 { + (self.next_range(1, 21)) as u8 + } +} diff --git a/game-core/src/render.rs b/game-core/src/render.rs new file mode 100644 index 0000000..4f35b9c --- /dev/null +++ b/game-core/src/render.rs @@ -0,0 +1,24 @@ +use crate::types::Stats; + +pub enum RenderRequest<'a> { + MainMenu, + CharacterCreation { + step: u8, + prompt: &'static str, + }, + Exploration { + tiles: &'a [u8], + width: u8, + player_idx: usize, + message: &'a str, + }, + Combat { + player_stats: &'a Stats, + enemy_stats: &'a Stats, + log: &'a str, + }, + Dialogue { + text: &'a str, + options: &'a [&'static str], + }, +} diff --git a/game-core/src/state.rs b/game-core/src/state.rs new file mode 100644 index 0000000..6d03745 --- /dev/null +++ b/game-core/src/state.rs @@ -0,0 +1,178 @@ +use crate::{ + assets::AssetLoader, + combat, + input::InputEvent, + map::MapView, + math::Rng, + render::RenderRequest, + types::{Stats, TileType}, +}; + +enum State { + Menu, + Creation, + Roaming, + Fighting, +} + +pub struct Game { + state: State, + + pub player: Stats, + pub player_pos_index: usize, + + map_buffer: [u8; 1024], + rng: Rng, + + last_message: &'static str, + enemy_temp: Stats, +} + +impl Game { + pub fn new() -> Self { + Self { + state: State::Menu, + player: Stats::default(), + player_pos_index: 0, + map_buffer: [0; 1024], + rng: Rng::new(12345), + last_message: "", + enemy_temp: Stats::default(), + } + } + + pub fn tick( + &'_ mut self, + input: InputEvent, + loader: &mut L, + ) -> RenderRequest<'_> { + match self.state { + State::Menu => { + if let InputEvent::Action = input { + self.state = State::Creation; + return RenderRequest::CharacterCreation { + step: 1, + prompt: "Rolling Stats...", + }; + } + RenderRequest::MainMenu + } + + State::Creation => { + self.player = Stats { + strength: self.rng.next_range(1, 20) as u8, + charisma: self.rng.next_range(1, 20) as u8, + intelligence: self.rng.next_range(1, 20) as u8, + constitution: self.rng.next_range(1, 20) as u8, + dexterity: self.rng.next_range(1, 20) as u8, + hp_current: 10, + hp_max: 10, + wisdom: self.rng.next_range(1, 20) as u8, + }; + + let _ = loader.load_map(1, &mut self.map_buffer); + + self.player_pos_index = 22; // Starting position arbitrary for mvp + self.state = State::Roaming; + self.last_message = "You find yourself in a mysterious land. Explore!"; + + RenderRequest::Exploration { + tiles: &self.map_buffer, + width: self.map_buffer[0], + player_idx: self.player_pos_index, + message: self.last_message, + } + } + + State::Roaming => self.handle_roaming(input), + + State::Fighting => self.handle_combat(input), + } + } + + fn handle_roaming(&'_ mut self, input: InputEvent) -> RenderRequest<'_> { + let view = MapView::new(&self.map_buffer); + + if let InputEvent::Move(dir) = input { + if let Some(new_idx) = view.get_neighbor_index(self.player_pos_index, dir) { + let tile = view.get_tile(new_idx); + match tile { + TileType::Wall => self.last_message = "You bump into a wall.", + TileType::Enemy => { + self.last_message = "An enemy appears!"; + self.state = State::Fighting; + self.enemy_temp = Stats { + strength: self.rng.next_range(1, 20) as u8, + charisma: self.rng.next_range(1, 20) as u8, + intelligence: self.rng.next_range(1, 20) as u8, + constitution: self.rng.next_range(1, 20) as u8, + dexterity: self.rng.next_range(1, 20) as u8, + hp_current: 10, + hp_max: 10, + wisdom: self.rng.next_range(1, 20) as u8, + }; + return RenderRequest::Combat { + player_stats: &self.player, + enemy_stats: &self.enemy_temp, + log: self.last_message, + }; + } + _ => { + self.player_pos_index = new_idx; + self.last_message = "Moving..."; + } + } + } + } + + RenderRequest::Exploration { + tiles: &self.map_buffer, + width: self.map_buffer[0], + player_idx: self.player_pos_index, + message: self.last_message, + } + } + + fn handle_combat(&'_ mut self, input: InputEvent) -> RenderRequest<'_> { + let mut log = "Your turn."; + + if let InputEvent::Action = input { + let (hit, dmg) = combat::resolve_attack(&mut self.rng, &self.player, &self.enemy_temp); + + if hit { + self.enemy_temp.hp_current -= dmg; + if self.enemy_temp.hp_current <= 0 { + self.state = State::Roaming; + // Clear the tile (hacky simple update) + // In real engine, we'd update an entity list, not the raw map buffer + self.last_message = "Victory!"; + return self.handle_roaming(InputEvent::None); + } + log = "Hit!"; + } else { + log = "You missed!"; + } + + // Enemy turn (simplified) + let (enemy_hit, enemy_dmg) = + combat::resolve_attack(&mut self.rng, &self.enemy_temp, &self.player); + if enemy_hit { + self.player.hp_current -= enemy_dmg; + if self.player.hp_current <= 0 { + self.state = State::Menu; + self.last_message = "You have been defeated. Returning to menu."; + return RenderRequest::MainMenu; + } + log = "Enemy hits you!"; + } else { + log = "Enemy missed!"; + } + } + + RenderRequest::Combat { + player_stats: &self.player, + enemy_stats: &self.enemy_temp, + log, + } + } +} diff --git a/game-core/src/types.rs b/game-core/src/types.rs new file mode 100644 index 0000000..6652260 --- /dev/null +++ b/game-core/src/types.rs @@ -0,0 +1,56 @@ +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum Direction { + North = 0, + South = 1, + East = 2, + West = 3, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum Attribute { + Strength = 0, + Dexterity = 1, + Constitution = 2, + Intelligence = 3, + Wisdom = 4, + Charisma = 5, +} + +#[derive(Clone, Copy, Debug, Default)] +#[repr(C)] +pub struct Stats { + pub strength: u8, + pub dexterity: u8, + pub constitution: u8, + pub intelligence: u8, + pub wisdom: u8, + pub charisma: u8, + pub hp_current: i8, + pub hp_max: i8, +} + +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub enum TileType { + Void = 0, + Grass = 1, + Water = 2, + Wall = 3, + Enemy = 4, + Npc = 5, +} + +impl From for TileType { + fn from(val: u8) -> Self { + match val { + 1 => TileType::Grass, + 2 => TileType::Water, + 3 => TileType::Wall, + 4 => TileType::Enemy, + 5 => TileType::Npc, + _ => TileType::Void, + } + } +} diff --git a/platforms/cli/.gitignore b/platforms/cli/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/platforms/cli/.gitignore @@ -0,0 +1 @@ +/target diff --git a/platforms/cli/Cargo.toml b/platforms/cli/Cargo.toml new file mode 100644 index 0000000..e19e925 --- /dev/null +++ b/platforms/cli/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cli" +version = "0.1.0" +edition = "2024" +default-run = "cli" + +[dependencies] +game-core = { path = "../../game-core" } diff --git a/platforms/cli/src/disk_loader.rs b/platforms/cli/src/disk_loader.rs new file mode 100644 index 0000000..083792d --- /dev/null +++ b/platforms/cli/src/disk_loader.rs @@ -0,0 +1,23 @@ +use game_core::assets::AssetLoader; + +pub struct DiskLoader; + +impl AssetLoader for DiskLoader { + fn load_chunk(&mut self, offset: u32, length: usize, dest: &mut [u8]) -> Result { + // In a real implementation, this would read from disk. + // For this example, we'll just fill the buffer with dummy data. + let dummy_data = vec![0u8; length]; + dest[..length].copy_from_slice(&dummy_data); + Ok(length) + } + + fn load_map(&mut self, map_id: u8, buffer: &mut [u8]) -> Result { + let map_data = [ + 5, 5, 2, 2, 2, 2, 2, 2, 1, 1, 4, 2, 2, 1, 3, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, + ]; + + let len = map_data.len(); + buffer[..len].copy_from_slice(&map_data); + Ok(len) + } +} diff --git a/platforms/cli/src/main.rs b/platforms/cli/src/main.rs new file mode 100644 index 0000000..bdbbbb0 --- /dev/null +++ b/platforms/cli/src/main.rs @@ -0,0 +1,93 @@ +use std::io::{self, Write}; + +use game_core::{input::InputEvent, render::RenderRequest, state::Game, types::Direction}; + +use crate::disk_loader::DiskLoader; + +mod disk_loader; + +fn main() { + let mut game = Game::new(); + let mut loader = DiskLoader; + + loop { + let mut input_str = String::new(); + print!("> "); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut input_str).unwrap(); + + let input = match input_str.trim() { + "w" => InputEvent::Move(Direction::North), + "s" => InputEvent::Move(Direction::South), + "a" => InputEvent::Move(Direction::West), + "d" => InputEvent::Move(Direction::East), + "e" => InputEvent::Action, + _ => InputEvent::None, + }; + + let render = game.tick(input, &mut loader); + + print!("{}[2J", 27 as char); // Clear screen + match render { + RenderRequest::MainMenu => { + println!("Welcome to the Twillight Island! Press 'e' to start.") + } + RenderRequest::CharacterCreation { prompt, .. } => { + println!("Creating Char: {}", prompt) + } + RenderRequest::Exploration { + tiles, + width, + player_idx, + message, + } => { + println!("STATUS: {}", message); + let w = width as usize; + // skip header (2 bytes) + let map_tiles = &tiles[2..]; + + for (i, &tile_byte) in map_tiles.iter().enumerate() { + if i % w == 0 { + println!(); + } + + if i == player_idx { + print!("@"); + } else { + let char = match tile_byte { + 1 => '.', // Grass + 2 => '~', // Water + 3 => '#', // Wall + 4 => 'E', // Enemy + _ => ' ', + }; + print!("{}", char); + } + } + println!(); + } + RenderRequest::Combat { + player_stats, + enemy_stats, + log, + } => { + println!("COMBAT!"); + println!( + "Player HP: {}/{}", + player_stats.hp_current, player_stats.hp_max + ); + println!( + "Enemy HP: {}/{}", + enemy_stats.hp_current, enemy_stats.hp_max + ); + println!("{}", log); + } + RenderRequest::Dialogue { text, options } => { + println!("{}", text); + for (i, option) in options.iter().enumerate() { + println!("{}: {}", i + 1, option); + } + } + } + } +} diff --git a/platforms/web/.gitignore b/platforms/web/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/platforms/web/.gitignore @@ -0,0 +1 @@ +/target diff --git a/platforms/web/Cargo.toml b/platforms/web/Cargo.toml new file mode 100644 index 0000000..52dfac4 --- /dev/null +++ b/platforms/web/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "web" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/platforms/web/src/main.rs b/platforms/web/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/platforms/web/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}