init
This commit is contained in:
1
game-core/.gitignore
vendored
Normal file
1
game-core/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
6
game-core/Cargo.toml
Normal file
6
game-core/Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "game-core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
5
game-core/src/assets.rs
Normal file
5
game-core/src/assets.rs
Normal 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
13
game-core/src/combat.rs
Normal 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
0
game-core/src/errors.rs
Normal file
8
game-core/src/input.rs
Normal file
8
game-core/src/input.rs
Normal 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
12
game-core/src/lib.rs
Normal 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
43
game-core/src/map.rs
Normal 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
27
game-core/src/math.rs
Normal 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
24
game-core/src/render.rs
Normal 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
178
game-core/src/state.rs
Normal 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
56
game-core/src/types.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user