This commit is contained in:
2026-02-09 15:13:50 +01:00
commit fb87c0639c
22 changed files with 530 additions and 0 deletions

1
game-core/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

6
game-core/Cargo.toml Normal file
View File

@@ -0,0 +1,6 @@
[package]
name = "game-core"
version = "0.1.0"
edition = "2024"
[dependencies]

5
game-core/src/assets.rs Normal file
View File

@@ -0,0 +1,5 @@
pub trait AssetLoader {
fn load_chunk(&mut self, offset: u32, length: usize, dest: &mut [u8]) -> Result<usize, ()>;
fn load_map(&mut self, map_id: u8, buffer: &mut [u8]) -> Result<usize, ()>;
}

13
game-core/src/combat.rs Normal file
View File

@@ -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)
}
}

0
game-core/src/errors.rs Normal file
View File

8
game-core/src/input.rs Normal file
View File

@@ -0,0 +1,8 @@
use crate::types::Direction;
pub enum InputEvent {
None,
Move(Direction),
Action,
Char(char),
}

12
game-core/src/lib.rs Normal file
View File

@@ -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;

43
game-core/src/map.rs Normal file
View File

@@ -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<usize> {
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)
}
}

27
game-core/src/math.rs Normal file
View File

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

24
game-core/src/render.rs Normal file
View File

@@ -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],
},
}

178
game-core/src/state.rs Normal file
View File

@@ -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<L: AssetLoader>(
&'_ 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,
}
}
}

56
game-core/src/types.rs Normal file
View File

@@ -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<u8> 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,
}
}
}