This commit is contained in:
nora 2023-04-17 19:35:19 +02:00
parent 40e412c024
commit 98dd54f1f2
18 changed files with 196 additions and 1445 deletions

View file

@ -1,5 +1,4 @@
use std::fmt::Write;
use anyhow::Context;
use directories::ProjectDirs;
use egui_sfml::SfEgui;
@ -7,26 +6,21 @@ use gamedebug_core::{imm, imm_dbg};
use sfml::{
audio::SoundSource,
graphics::{
BlendMode, Color, Rect, RectangleShape, RenderStates, RenderTarget, RenderTexture,
RenderWindow, Shape, Sprite, Transformable, View,
BlendMode, Color, Rect, RectangleShape, RenderStates, RenderTarget,
RenderTexture, RenderWindow, Shape, Sprite, Transformable, View,
},
system::{Vector2, Vector2u},
window::{Event, Key},
};
use crate::{
command::{Cmd, CmdVec},
debug::{self, DebugState},
game::{for_each_tile_on_screen, Biome, GameState},
graphics::{self, ScreenSc, ScreenVec},
input::Input,
inventory::{ItemId, Slot, TileLayer, UseAction},
input::Input, inventory::{ItemId, Slot, TileLayer, UseAction},
math::{center_offset, TILE_SIZE},
res::Res,
tiles::TileId,
CliArgs,
res::Res, tiles::TileId, CliArgs,
};
/// Application level state (includes game and ui state, etc.)
pub struct App {
pub rw: RenderWindow,
@ -45,39 +39,10 @@ pub struct App {
pub project_dirs: ProjectDirs,
pub cmdvec: CmdVec,
}
impl App {
pub fn new(args: CliArgs) -> anyhow::Result<Self> {
let rw = graphics::make_window();
let sf_egui = SfEgui::new(&rw);
let mut res = Res::load()?;
res.surf_music.set_looping(true);
res.surf_music.set_volume(10.0);
res.surf_music.play();
let rw_size = rw.size();
let rt =
RenderTexture::new(rw_size.x, rw_size.y).context("Failed to create render texture")?;
let light_map = RenderTexture::new(rw_size.x, rw_size.y)
.context("Failed to create lightmap texture")?;
let project_dirs = ProjectDirs::from("", "", "mantle-diver").unwrap();
let worlds_dir = project_dirs.data_dir().join("worlds");
let path = worlds_dir.join(&args.world_name);
Ok(Self {
rw,
should_quit: false,
game: GameState::new(args.world_name, path, &res),
res,
sf_egui,
input: Input::default(),
debug: DebugState::default(),
scale: 1,
rt,
light_map,
project_dirs,
cmdvec: CmdVec::default(),
})
loop {}
}
pub fn do_game_loop(&mut self) {
while !self.should_quit {
self.do_event_handling();
@ -89,252 +54,15 @@ impl App {
self.game.tile_db.try_save();
self.game.world.save();
}
fn do_event_handling(&mut self) {
while let Some(ev) = self.rw.poll_event() {
self.sf_egui.add_event(&ev);
{
let ctx = self.sf_egui.context();
self.input.update_from_event(
&ev,
ctx.wants_keyboard_input(),
ctx.wants_pointer_input(),
);
}
match ev {
Event::Closed => self.should_quit = true,
Event::Resized { width, height } => {
self.rt =
RenderTexture::new(width / self.scale as u32, height / self.scale as u32)
.unwrap();
self.light_map =
RenderTexture::new(width / self.scale as u32, height / self.scale as u32)
.unwrap();
let view = View::from_rect(Rect::new(0., 0., width as f32, height as f32));
self.rw.set_view(&view);
}
Event::KeyPressed { code, .. } => match code {
Key::F11 => {
self.debug.console.show ^= true;
self.debug.console.just_opened = true;
}
Key::F12 => self.debug.panel ^= true,
_ => {}
},
_ => {}
}
}
loop {}
}
fn do_update(&mut self) {
let rt_size = self.rt.size();
if self.debug.freecam {
self.do_freecam();
} else {
let spd = if self.input.down(Key::LShift) {
8.0
} else if self.input.down(Key::LControl) {
128.0
} else {
3.0
};
self.game.world.player.hspeed = 0.;
if self.input.down(Key::A) {
self.game.world.player.hspeed = -spd;
}
if self.input.down(Key::D) {
self.game.world.player.hspeed = spd;
}
if self.input.down(Key::W) && self.game.world.player.can_jump() {
self.game.world.player.vspeed = -10.0;
self.game.world.player.jumps_left = 0;
}
self.game.world.player.down_intent = self.input.down(Key::S);
let terminal_velocity = 60.0;
self.game.world.player.vspeed = self
.game
.world
.player
.vspeed
.clamp(-terminal_velocity, terminal_velocity);
let mut on_screen_tile_ents = Vec::new();
for_each_tile_on_screen(self.game.camera_offset, self.rt.size(), |tp, _sp| {
let tile = self.game.world.tile_at_mut(tp, &self.game.worldgen).mid;
if tile.empty() {
return;
}
let tdef = &self.game.tile_db[tile];
let Some(bb) = tdef.layer.bb else {
return;
};
let x = tp.x as i32 * TILE_SIZE as i32;
let y = tp.y as i32 * TILE_SIZE as i32;
let en = s2dc::Entity::from_rect_corners(
x + bb.x as i32,
y + bb.y as i32,
x + bb.w as i32,
y + bb.h as i32,
);
on_screen_tile_ents.push(TileColEn {
col: en,
platform: tdef.layer.platform,
});
});
imm_dbg!(on_screen_tile_ents.len());
self.game.world.player.col_en.move_y(
self.game.world.player.vspeed,
|player_en, off| {
let mut col = false;
for en in &on_screen_tile_ents {
if player_en.would_collide(&en.col, off) {
if en.platform {
if self.game.world.player.vspeed < 0. {
continue;
}
// If the player's feet are below the top of the platform,
// collision shouldn't happen
let player_feet = player_en.pos.y + player_en.bb.y;
if player_feet > en.col.pos.y || self.game.world.player.down_intent
{
continue;
}
}
col = true;
if self.game.world.player.vspeed > 0. {
self.game.world.player.jumps_left = 1;
}
self.game.world.player.vspeed = 0.;
}
}
col
},
);
self.game.world.player.col_en.move_x(
self.game.world.player.hspeed,
|player_en, off| {
let mut col = false;
for en in &on_screen_tile_ents {
if en.platform {
continue;
}
if player_en.would_collide(&en.col, off) {
col = true;
self.game.world.player.hspeed = 0.;
}
}
col
},
);
self.game.world.player.vspeed += self.game.gravity;
let (x, y, _w, _h) = self.game.world.player.col_en.en.xywh();
self.game.camera_offset.x = (x - rt_size.x as i32 / 2).try_into().unwrap_or(0);
self.game.camera_offset.y = (y - rt_size.y as i32 / 2).try_into().unwrap_or(0);
}
let mut loc = self.input.mouse_down_loc;
let vco = viewport_center_offset(self.rw.size(), rt_size, self.scale);
loc.x -= vco.x;
loc.y -= vco.y;
loc.x /= self.scale as ScreenSc;
loc.y /= self.scale as ScreenSc;
let mut wpos = self.game.camera_offset;
wpos.x = wpos.x.saturating_add_signed(loc.x.into());
wpos.y = wpos.y.saturating_add_signed(loc.y.into());
let mouse_tpos = wpos.tile_pos();
imm!(
"Mouse @ tile {}, {} ({:?})",
mouse_tpos.x,
mouse_tpos.y,
self.game.world.tile_at_mut(mouse_tpos, &self.game.worldgen)
);
let m_chk = mouse_tpos.to_chunk();
imm!("@ chunk {}, {}", m_chk.x, m_chk.y);
let (m_chk_x, m_chk_y) = m_chk.region();
imm!("@ region {m_chk_x}, {m_chk_y}");
if self.debug.freecam && self.input.pressed(Key::P) {
self.game.world.player.col_en.en.pos.x = wpos.x as i32;
self.game.world.player.col_en.en.pos.y = wpos.y as i32;
}
'item_use: {
if !self.input.lmb_down {
break 'item_use;
}
let Some(active_slot) = self.game.inventory.slots.get(self.game.selected_inv_slot) else {
log::error!("Selected slot {} out of bounds", self.game.selected_inv_slot);
break 'item_use;
};
if active_slot.qty == 0 {
break 'item_use;
}
let def = &self.game.itemdb.db[active_slot.id as usize];
let t = self.game.world.tile_at_mut(mouse_tpos, &self.game.worldgen);
match &def.use_action {
UseAction::PlaceBgTile { id } => {
if t.bg.empty() {
t.bg = *id
}
}
UseAction::PlaceMidTile { id } => {
if t.mid.empty() {
t.mid = *id
}
}
UseAction::PlaceFgTile { id } => {
if t.fg.empty() {
t.fg = *id
}
}
UseAction::RemoveTile { layer } => match layer {
TileLayer::Bg => t.bg = TileId::EMPTY,
TileLayer::Mid => t.mid = TileId::EMPTY,
TileLayer::Fg => t.fg = TileId::EMPTY,
},
}
}
if self.game.camera_offset.y > 643_000 {
self.game.current_biome = Biome::Underground;
} else {
self.game.current_biome = Biome::Surface;
}
if self.game.current_biome != self.game.prev_biome {
self.game.prev_biome = self.game.current_biome;
match self.game.current_biome {
Biome::Surface => {
self.res.und_music.stop();
self.res.surf_music.play();
}
Biome::Underground => {
self.res.surf_music.stop();
self.res.und_music.set_volume(self.res.surf_music.volume());
self.res.und_music.set_looping(true);
self.res.und_music.play();
}
}
}
self.game.update(&self.input);
loop {}
}
fn do_freecam(&mut self) {
let spd = if self.input.down(Key::LShift) {
100
} else if self.input.down(Key::LControl) {
1000
} else {
2
};
if self.input.down(Key::A) {
self.game.camera_offset.x = self.game.camera_offset.x.saturating_sub(spd);
}
if self.input.down(Key::D) {
self.game.camera_offset.x = self.game.camera_offset.x.saturating_add(spd);
}
if self.input.down(Key::W) {
self.game.camera_offset.y = self.game.camera_offset.y.saturating_sub(spd);
}
if self.input.down(Key::S) {
self.game.camera_offset.y = self.game.camera_offset.y.saturating_add(spd);
}
loop {}
}
fn do_rendering(&mut self) {
self.game.light_pass(&mut self.light_map, &self.res);
self.rt.clear(Color::rgb(55, 221, 231));
@ -347,14 +75,12 @@ impl App {
spr.set_position((vco.x as f32, vco.y as f32));
self.rw.clear(Color::rgb(40, 10, 70));
self.rw.draw(&spr);
// Draw light overlay with multiply blending
let mut rst = RenderStates::default();
rst.blend_mode = BlendMode::MULTIPLY;
self.light_map.display();
spr.set_texture(self.light_map.texture(), false);
self.rw.draw_with_renderstates(&spr, &rst);
drop(spr);
// Draw ui on top of in-game scene
self.rt.clear(Color::TRANSPARENT);
let ui_dims = Vector2 {
x: (self.rw.size().x / self.scale as u32) as f32,
@ -380,7 +106,9 @@ impl App {
if self.debug.show_atlas {
let atlas = &self.res.atlas.tex;
let size = atlas.size();
let mut rs = RectangleShape::from_rect(Rect::new(0., 0., size.x as f32, size.y as f32));
let mut rs = RectangleShape::from_rect(
Rect::new(0., 0., size.x as f32, size.y as f32),
);
rs.set_fill_color(Color::MAGENTA);
self.rw.draw(&rs);
self.rw.draw(&Sprite::with_texture(atlas));
@ -390,58 +118,15 @@ impl App {
drop(spr);
self.execute_commands();
}
fn execute_commands(&mut self) {
for cmd in self.cmdvec.drain(..) {
match cmd {
Cmd::QuitApp => self.should_quit = true,
Cmd::ToggleFreecam => self.debug.freecam ^= true,
Cmd::TeleportPlayer { pos, relative } => {
if relative {
let s2dc = pos.to_s2dc();
self.game.world.player.col_en.en.pos.x += s2dc.x;
self.game.world.player.col_en.en.pos.y += s2dc.y;
} else {
self.game.world.player.col_en.en.pos = pos.to_s2dc()
}
}
Cmd::TeleportPlayerSpawn => {
self.game.world.player.col_en.en.pos = self.game.spawn_point.to_s2dc()
}
Cmd::GiveItemByName(name) => {
for (i, item) in self.game.itemdb.db.iter().enumerate() {
if item.name == name {
self.game.inventory.slots.push(Slot {
id: i as ItemId,
qty: 1,
});
return;
}
}
writeln!(
&mut self.debug.console.log,
"Item with name '{name}' not found"
)
.unwrap();
}
}
}
loop {}
}
}
/// Tile collision entity for doing physics
struct TileColEn {
col: s2dc::Entity,
platform: bool,
}
fn viewport_center_offset(rw_size: Vector2u, rt_size: Vector2u, scale: u8) -> ScreenVec {
let rw_size = rw_size;
let rt_size = rt_size * scale as u32;
let x = center_offset(rt_size.x as i32, rw_size.x as i32);
let y = center_offset(rt_size.y as i32, rw_size.y as i32);
ScreenVec {
x: x as ScreenSc,
y: y as ScreenSc,
}
loop {}
}

View file

@ -1,58 +1,22 @@
use std::ops::{BitAndAssign, BitOrAssign};
use num_traits::PrimInt;
pub fn nth_bit_set<N: PrimInt>(number: N, n: usize) -> bool {
(number & (N::one() << n)) != N::zero()
loop {}
}
pub fn set_nth_bit<N: PrimInt + BitOrAssign + BitAndAssign>(number: &mut N, n: usize, set: bool) {
let mask = N::one() << n;
if set {
*number |= mask;
} else {
*number &= !mask;
}
pub fn set_nth_bit<N: PrimInt + BitOrAssign + BitAndAssign>(
number: &mut N,
n: usize,
set: bool,
) {
loop {}
}
#[test]
#[allow(clippy::bool_assert_comparison)]
fn test_nth_bit_set() {
let number: u8 = 0b0100_0100;
assert_eq!(nth_bit_set(number, 0), false);
assert_eq!(nth_bit_set(number, 1), false);
assert_eq!(nth_bit_set(number, 2), true);
assert_eq!(nth_bit_set(number, 3), false);
assert_eq!(nth_bit_set(number, 4), false);
assert_eq!(nth_bit_set(number, 5), false);
assert_eq!(nth_bit_set(number, 6), true);
assert_eq!(nth_bit_set(number, 7), false);
assert_eq!(nth_bit_set(0u64, 0), false);
assert_eq!(nth_bit_set(u64::MAX, 63), true);
loop {}
}
#[test]
#[allow(clippy::bool_assert_comparison)]
fn test_set_nth_bit() {
let mut number: u8 = 0b0000_0000;
set_nth_bit(&mut number, 0, true);
assert_eq!(number, 0b0000_0001);
set_nth_bit(&mut number, 1, true);
assert_eq!(number, 0b0000_0011);
set_nth_bit(&mut number, 2, true);
assert_eq!(number, 0b0000_0111);
set_nth_bit(&mut number, 0, false);
assert_eq!(number, 0b0000_0110);
let mut all_bits_set: u64 = 0;
for i in 0..64 {
set_nth_bit(&mut all_bits_set, i, true);
assert_eq!(nth_bit_set(all_bits_set, i), true);
}
let mut no_bits_set: u64 = u64::MAX;
for i in 0..64 {
set_nth_bit(&mut no_bits_set, i, false);
assert_eq!(nth_bit_set(no_bits_set, i), false);
}
loop {}
}

View file

@ -1,7 +1,5 @@
use clap::Parser;
use crate::{command::Cmd, math::WorldPos};
#[derive(Parser)]
pub enum CmdLine {
Quit,
@ -11,7 +9,6 @@ pub enum CmdLine {
Spawn,
Give(Give),
}
#[derive(Parser)]
pub struct Tp {
x: u32,
@ -22,40 +19,22 @@ pub struct Tp {
}
impl Tp {
fn to_world_pos(&self) -> WorldPos {
WorldPos {
x: self.x,
y: self.y,
}
loop {}
}
}
#[derive(Parser)]
pub struct Give {
name: String,
}
pub enum Dispatch {
Cmd(Cmd),
ClearConsole,
}
impl CmdLine {
pub fn parse_cmdline(cmdline: &str) -> anyhow::Result<Self> {
let words = std::iter::once(" ").chain(cmdline.split_whitespace());
Ok(Self::try_parse_from(words)?)
loop {}
}
pub(crate) fn dispatch(self) -> Dispatch {
match self {
CmdLine::Quit => Dispatch::Cmd(Cmd::QuitApp),
CmdLine::Freecam => Dispatch::Cmd(Cmd::ToggleFreecam),
CmdLine::Clear => Dispatch::ClearConsole,
CmdLine::Tp(tp) => Dispatch::Cmd(Cmd::TeleportPlayer {
pos: tp.to_world_pos(),
relative: tp.rel,
}),
CmdLine::Spawn => Dispatch::Cmd(Cmd::TeleportPlayerSpawn),
CmdLine::Give(give) => Dispatch::Cmd(Cmd::GiveItemByName(give.name)),
}
loop {}
}
}

View file

@ -1,20 +1,13 @@
use std::fmt::Write;
use egui::TextBuffer;
use egui_inspect::{derive::Inspect, inspect};
use sfml::audio::SoundSource;
use crate::{
cmdline::CmdLine,
command::CmdVec,
game::GameState,
cmdline::CmdLine, command::CmdVec, game::GameState,
math::{px_per_frame_to_km_h, WorldPos},
res::Res,
stringfmt::LengthDisp,
texture_atlas::AtlasBundle,
res::Res, stringfmt::LengthDisp, texture_atlas::AtlasBundle,
tiles::tiledb_edit_ui::TileDbEdit,
};
#[derive(Default, Debug, Inspect)]
pub struct DebugState {
pub panel: bool,
@ -23,7 +16,6 @@ pub struct DebugState {
pub show_atlas: bool,
pub console: Console,
}
#[derive(Default, Debug, Inspect)]
pub struct Console {
pub show: bool,
@ -32,7 +24,6 @@ pub struct Console {
pub just_opened: bool,
pub history: Vec<String>,
}
fn debug_panel_ui(
mut debug: &mut DebugState,
mut game: &mut GameState,
@ -40,75 +31,92 @@ fn debug_panel_ui(
res: &mut Res,
mut scale: &mut u8,
) {
egui::Window::new("Debug (F12)").show(ctx, |ui| {
if debug.freecam {
ui.label("Cam x");
ui.add(egui::DragValue::new(&mut game.camera_offset.x));
ui.label("Cam y");
ui.add(egui::DragValue::new(&mut game.camera_offset.y));
let co = game.camera_offset;
ui.label(format!(
"Cam Depth: {}",
LengthDisp(co.y as f32 - WorldPos::SURFACE as f32)
));
ui.label(format!(
"Cam offset from center: {}",
LengthDisp(co.x as f32 - WorldPos::CENTER as f32)
));
} else {
ui.label(format!(
"Player Depth: {}",
LengthDisp(game.world.player.feet_y() as f32 - WorldPos::SURFACE as f32)
));
ui.label(format!(
"Player offset from center: {}",
LengthDisp(game.world.player.col_en.en.pos.x as f32 - WorldPos::CENTER as f32)
));
ui.label(format!(
"Hspeed: {} ({} km/h)",
game.world.player.hspeed,
px_per_frame_to_km_h(game.world.player.hspeed)
));
ui.label(format!(
"Vspeed: {} ({} km/h)",
game.world.player.vspeed,
px_per_frame_to_km_h(game.world.player.vspeed)
));
}
ui.label("Music volume");
let mut vol = res.surf_music.volume();
ui.add(egui::DragValue::new(&mut vol));
res.surf_music.set_volume(vol);
ui.separator();
egui::ScrollArea::both()
.id_source("insp_scroll")
.max_height(240.)
.max_width(340.0)
.show(ui, |ui| {
inspect! {
ui,
scale,
game,
debug
egui::Window::new("Debug (F12)")
.show(
ctx,
|ui| {
if debug.freecam {
ui.label("Cam x");
ui.add(egui::DragValue::new(&mut game.camera_offset.x));
ui.label("Cam y");
ui.add(egui::DragValue::new(&mut game.camera_offset.y));
let co = game.camera_offset;
ui.label(
format!(
"Cam Depth: {}", LengthDisp(co.y as f32 - WorldPos::SURFACE
as f32)
),
);
ui.label(
format!(
"Cam offset from center: {}", LengthDisp(co.x as f32 -
WorldPos::CENTER as f32)
),
);
} else {
ui.label(
format!(
"Player Depth: {}", LengthDisp(game.world.player.feet_y() as
f32 - WorldPos::SURFACE as f32)
),
);
ui.label(
format!(
"Player offset from center: {}", LengthDisp(game.world.player
.col_en.en.pos.x as f32 - WorldPos::CENTER as f32)
),
);
ui.label(
format!(
"Hspeed: {} ({} km/h)", game.world.player.hspeed,
px_per_frame_to_km_h(game.world.player.hspeed)
),
);
ui.label(
format!(
"Vspeed: {} ({} km/h)", game.world.player.vspeed,
px_per_frame_to_km_h(game.world.player.vspeed)
),
);
}
});
if ui.button("Reload graphics").clicked() {
res.atlas = AtlasBundle::new().unwrap();
game.tile_db.update_rects(&res.atlas.rects);
}
ui.separator();
egui::ScrollArea::vertical().show(ui, |ui| {
gamedebug_core::for_each_imm(|info| match info {
gamedebug_core::Info::Msg(msg) => {
ui.label(msg);
ui.label("Music volume");
let mut vol = res.surf_music.volume();
ui.add(egui::DragValue::new(&mut vol));
res.surf_music.set_volume(vol);
ui.separator();
egui::ScrollArea::both()
.id_source("insp_scroll")
.max_height(240.)
.max_width(340.0)
.show(
ui,
|ui| {
inspect! {
ui, scale, game, debug
}
},
);
if ui.button("Reload graphics").clicked() {
res.atlas = AtlasBundle::new().unwrap();
game.tile_db.update_rects(&res.atlas.rects);
}
gamedebug_core::Info::Rect(_, _, _, _, _) => todo!(),
});
});
gamedebug_core::clear_immediates();
});
ui.separator();
egui::ScrollArea::vertical()
.show(
ui,
|ui| {
gamedebug_core::for_each_imm(|info| match info {
gamedebug_core::Info::Msg(msg) => {
ui.label(msg);
}
gamedebug_core::Info::Rect(_, _, _, _, _) => todo!(),
});
},
);
gamedebug_core::clear_immediates();
},
);
}
pub(crate) fn do_debug_ui(
ctx: &egui::Context,
debug: &mut DebugState,
@ -125,42 +133,6 @@ pub(crate) fn do_debug_ui(
console_ui(ctx, debug, cmd);
}
}
fn console_ui(ctx: &egui::Context, debug: &mut DebugState, cmd: &mut CmdVec) {
egui::Window::new("Console (F11)").show(ctx, |ui| {
let up_arrow =
ui.input_mut(|inp| inp.consume_key(egui::Modifiers::default(), egui::Key::ArrowUp));
let re =
ui.add(egui::TextEdit::singleline(&mut debug.console.cmdline).hint_text("Command"));
if debug.console.just_opened {
re.request_focus();
}
if re.lost_focus() && ui.input(|inp| inp.key_pressed(egui::Key::Enter)) {
re.request_focus();
let cmdline = match CmdLine::parse_cmdline(&debug.console.cmdline) {
Ok(cmd) => cmd,
Err(e) => {
writeln!(&mut debug.console.log, "{e}").unwrap();
debug.console.history.push(debug.console.cmdline.take());
return;
}
};
debug.console.history.push(debug.console.cmdline.take());
match cmdline.dispatch() {
crate::cmdline::Dispatch::Cmd(command) => cmd.push(command),
crate::cmdline::Dispatch::ClearConsole => debug.console.log.clear(),
}
}
if up_arrow {
if let Some(line) = debug.console.history.pop() {
debug.console.cmdline = line;
}
}
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.show(ui, |ui| {
ui.add(egui::TextEdit::multiline(&mut &debug.console.log[..]));
});
});
debug.console.just_opened = false;
loop {}
}

View file

@ -1,26 +1,21 @@
use std::path::PathBuf;
use derivative::Derivative;
use egui_inspect::derive::Inspect;
use sfml::{
graphics::{
Color, Rect, RectangleShape, RenderTarget, RenderTexture, Shape, Sprite, Transformable,
Color, Rect, RectangleShape, RenderTarget, RenderTexture, Shape, Sprite,
Transformable,
},
system::{Vector2f, Vector2u},
window::Key,
};
use crate::{
graphics::{ScreenSc, ScreenVec},
input::Input,
inventory::{Inventory, ItemDb},
input::Input, inventory::{Inventory, ItemDb},
math::{smoothwave, wp_to_tp, WorldPos},
res::Res,
tiles::TileDb,
world::{TilePos, World},
res::Res, tiles::TileDb, world::{TilePos, World},
worldgen::Worldgen,
};
#[derive(Derivative, Inspect)]
#[derivative(Debug)]
pub struct GameState {
@ -40,186 +35,39 @@ pub struct GameState {
pub selected_inv_slot: usize,
pub spawn_point: WorldPos,
}
#[derive(Debug, Inspect)]
pub struct LightSource {
pub pos: ScreenVec,
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, Inspect)]
pub enum Biome {
Surface,
Underground,
}
impl GameState {
pub fn update(&mut self, input: &Input) {
if input.pressed(Key::Num1) {
self.selected_inv_slot = 0;
}
if input.pressed(Key::Num2) {
self.selected_inv_slot = 1;
}
if input.pressed(Key::Num3) {
self.selected_inv_slot = 2;
}
if input.pressed(Key::Num4) {
self.selected_inv_slot = 3;
}
if input.pressed(Key::Num5) {
self.selected_inv_slot = 4;
}
if input.pressed(Key::Num6) {
self.selected_inv_slot = 5;
}
if input.pressed(Key::Num7) {
self.selected_inv_slot = 6;
}
if input.pressed(Key::Num8) {
self.selected_inv_slot = 7;
}
if input.pressed(Key::Num9) {
self.selected_inv_slot = 8;
}
if input.pressed(Key::Num0) {
self.selected_inv_slot = 9;
}
self.world.ticks += 1;
loop {}
}
pub(crate) fn draw_world(&mut self, rt: &mut RenderTexture, res: &mut Res) {
self.light_sources.clear();
let mut s = Sprite::with_texture(&res.atlas.tex);
for_each_tile_on_screen(self.camera_offset, rt.size(), |tp, sp| {
let tile = self.world.tile_at_mut(tp, &self.worldgen);
s.set_position(sp.to_sf_vec());
if !tile.bg.empty() {
s.set_texture_rect(self.tile_db[tile.bg].tex_rect.to_sf());
rt.draw(&s);
}
if !tile.mid.empty() {
s.set_texture_rect(self.tile_db[tile.mid].tex_rect.to_sf());
if let Some(light) = self.tile_db[tile.mid].light {
let pos = ScreenVec {
x: sp.x + light.x,
y: sp.y + light.y,
};
self.light_sources.push(LightSource { pos });
}
rt.draw(&s);
}
if !tile.fg.empty() {
s.set_texture_rect(self.tile_db[tile.fg].tex_rect.to_sf());
rt.draw(&s);
}
});
loop {}
}
pub fn draw_entities(&mut self, rt: &mut RenderTexture) {
let (x, y, w, h) = self.world.player.col_en.en.xywh();
let mut rect_sh = RectangleShape::new();
rect_sh.set_position((
(x - self.camera_offset.x as i32) as f32,
(y - self.camera_offset.y as i32) as f32,
));
rect_sh.set_size((w as f32, h as f32));
rt.draw(&rect_sh);
rect_sh.set_size((2., 2.));
rect_sh.set_fill_color(Color::RED);
rect_sh.set_position((
(self.world.player.col_en.en.pos.x - self.camera_offset.x as i32) as f32,
(self.world.player.col_en.en.pos.y - self.camera_offset.y as i32) as f32,
));
rt.draw(&rect_sh);
loop {}
}
pub fn draw_ui(&mut self, rt: &mut RenderTexture, res: &Res, ui_dims: Vector2f) {
let mut s = Sprite::with_texture(&res.atlas.tex);
let mut rs = RectangleShape::from_rect(Rect::new(0., 0., 32., 32.));
rs.set_outline_color(Color::YELLOW);
rs.set_outline_thickness(1.0);
rs.set_fill_color(Color::TRANSPARENT);
for (i, slot) in self.inventory.slots.iter().enumerate() {
s.set_texture_rect(res.atlas.rects["ui/invframe"].to_sf());
let pos = ((i * 38) as f32 + 8.0, (ui_dims.y - 38.));
s.set_position(pos);
rt.draw(&s);
let item_def = &self.itemdb.db[slot.id as usize];
if let Some(rect) = res.atlas.rects.get(&item_def.graphic_name) {
s.set_texture_rect(rect.to_sf());
rt.draw(&s);
} else {
log::error!("Missing rect for item {}", item_def.name);
}
if i == self.selected_inv_slot {
rs.set_position(pos);
rt.draw(&rs);
}
}
loop {}
}
pub(crate) fn light_pass(&mut self, lightmap: &mut RenderTexture, res: &Res) {
let how_much_below_surface = self.camera_offset.y.saturating_sub(WorldPos::SURFACE);
let light_dropoff = (how_much_below_surface / 8).min(255) as u8;
let daylight = 255u8;
self.ambient_light = daylight.saturating_sub(light_dropoff);
// Clear light map
// You can clear to a brighter color to increase ambient light level
lightmap.clear(Color::rgba(
self.ambient_light,
self.ambient_light,
self.ambient_light,
255,
));
let mut s = Sprite::with_texture(&res.atlas.tex);
s.set_texture_rect(res.atlas.rects["light/1"].to_sf());
for ls in &self.light_sources {
let flicker = smoothwave(self.world.ticks, 40) as f32 / 64.0;
s.set_scale((4. + flicker, 4. + flicker));
s.set_origin((128., 128.));
s.set_position((ls.pos.x.into(), ls.pos.y.into()));
lightmap.draw(&s);
}
loop {}
}
pub(crate) fn new(world_name: String, path: PathBuf, res: &Res) -> GameState {
let mut spawn_point = WorldPos::SURFACE_CENTER;
spawn_point.y -= 1104;
let mut tile_db = TileDb::load_or_default();
tile_db.update_rects(&res.atlas.rects);
Self {
camera_offset: spawn_point,
world: World::new(spawn_point, &world_name, path),
gravity: 0.55,
current_biome: Biome::Surface,
prev_biome: Biome::Surface,
worldgen: Worldgen::from_seed(0),
ambient_light: 0,
light_sources: Vec::new(),
tile_db,
inventory: Inventory::new_debug(),
itemdb: ItemDb::default(),
selected_inv_slot: 0,
spawn_point,
}
loop {}
}
}
pub fn for_each_tile_on_screen(
camera_offset: WorldPos,
rt_size: Vector2u,
mut f: impl FnMut(TilePos, ScreenVec),
) {
for y in (-32..(rt_size.y as i16) + 32).step_by(32) {
for x in (-32..(rt_size.x as i16) + 32).step_by(32) {
f(
TilePos {
x: wp_to_tp(camera_offset.x.saturating_add(x.try_into().unwrap_or(0))),
y: wp_to_tp(camera_offset.y.saturating_add(y.try_into().unwrap_or(0))),
},
ScreenVec {
x: ((x as i64) - ((camera_offset.x as i64) % 32)) as ScreenSc,
y: ((y as i64) - ((camera_offset.y as i64) % 32)) as ScreenSc,
},
)
}
}
loop {}
}

View file

@ -1,55 +1,33 @@
use egui_inspect::derive::Inspect;
use serde::{Deserialize, Serialize};
use sfml::{
graphics::RenderWindow,
system::Vector2f,
window::{ContextSettings, Style, VideoMode},
graphics::RenderWindow, system::Vector2f, window::{ContextSettings, Style, VideoMode},
};
use sfml_xt::graphics::RenderWindowExt;
use crate::math::FPS_TARGET;
pub struct ScreenRes {
pub w: u16,
pub h: u16,
}
impl ScreenRes {
fn to_sf(&self) -> VideoMode {
VideoMode {
width: self.w as _,
height: self.h as _,
bits_per_pixel: 32,
}
loop {}
}
}
#[derive(Default, Clone, Copy, Debug, Inspect, Serialize, Deserialize)]
pub struct ScreenVec {
pub x: ScreenSc,
pub y: ScreenSc,
}
/// Screen position/offset scalar
/// We assume this game won't be played above 32767*32767 resolution
pub type ScreenSc = i16;
impl ScreenVec {
pub fn to_sf_vec(self) -> Vector2f {
Vector2f::new(self.x.into(), self.y.into())
loop {}
}
}
const DEFAULT_RES: ScreenRes = ScreenRes { w: 960, h: 540 };
pub fn make_window() -> RenderWindow {
let mut rw = RenderWindow::new(
DEFAULT_RES.to_sf(),
"Mantle Diver",
Style::DEFAULT,
&ContextSettings::default(),
);
rw.set_framerate_limit(FPS_TARGET.into());
rw.center();
rw
loop {}
}

View file

@ -1,8 +1,6 @@
use fnv::FnvHashSet;
use sfml::window::{mouse, Event, Key};
use crate::graphics::ScreenVec;
#[derive(Default, Debug)]
pub struct Input {
down: FnvHashSet<Key>,
@ -12,65 +10,18 @@ pub struct Input {
pub mouse_down_loc: ScreenVec,
pub mid_pressed: bool,
}
impl Input {
pub fn update_from_event(&mut self, ev: &Event, egui_kbd: bool, egui_ptr: bool) {
match ev {
&Event::KeyPressed { code, .. } => {
self.pressed.insert(code);
self.down.insert(code);
}
Event::KeyReleased { code, .. } => {
self.down.remove(code);
}
&Event::MouseButtonPressed { button, x, y } => {
self.mouse_down_loc = ScreenVec {
x: x as i16,
y: y as i16,
};
if button == mouse::Button::Left {
self.lmb_down = true;
}
if button == mouse::Button::Right {
self.rmb_down = true;
}
if button == mouse::Button::Middle {
self.mid_pressed = true;
}
}
&Event::MouseButtonReleased { button, .. } => {
if button == mouse::Button::Left {
self.lmb_down = false;
}
if button == mouse::Button::Right {
self.rmb_down = false;
}
}
&Event::MouseMoved { x, y } => {
self.mouse_down_loc.x = x as i16;
self.mouse_down_loc.y = y as i16;
}
_ => {}
}
if egui_kbd {
self.pressed.clear();
self.down.clear();
}
if egui_ptr {
self.lmb_down = false;
self.rmb_down = false;
self.mid_pressed = false;
}
loop {}
}
/// Pressed event should be cleared every frame
pub fn clear_pressed(&mut self) {
self.mid_pressed = false;
self.pressed.clear();
loop {}
}
pub fn down(&self, key: Key) -> bool {
self.down.contains(&key)
loop {}
}
pub fn pressed(&self, key: Key) -> bool {
self.pressed.contains(&key)
loop {}
}
}

View file

@ -1,22 +1,15 @@
use egui_inspect::derive::Inspect;
use crate::{
math::IntRect,
tiles::{BgTileId, FgTileId, MidTileId},
};
use crate::{math::IntRect, tiles::{BgTileId, FgTileId, MidTileId}};
/// We won't have more than 65535 different items
pub type ItemId = u16;
/// We won't have more than 65535 item quantity in a single slot
pub type ItemQty = u16;
/// Inventory slot
#[derive(Debug, Inspect)]
pub struct Slot {
pub id: ItemId,
pub qty: ItemQty,
}
#[derive(Debug, Inspect)]
pub struct Inventory {
pub slots: Vec<Slot>,
@ -24,37 +17,9 @@ pub struct Inventory {
impl Inventory {
/// A new inventory filled with some debug items
pub(crate) fn new_debug() -> Inventory {
Self {
slots: vec![
Slot {
id: items::WOOD_PICK,
qty: 1,
},
Slot {
id: items::DIRT_BLOCK,
qty: 100,
},
Slot {
id: items::TORCH,
qty: 100,
},
Slot {
id: items::PLATFORM,
qty: 100,
},
Slot {
id: items::STONE_WALL,
qty: 100,
},
Slot {
id: items::PANZERIUM,
qty: 100,
},
],
}
loop {}
}
}
#[derive(Debug, Inspect)]
pub struct ItemDef {
pub name: String,
@ -64,14 +29,12 @@ pub struct ItemDef {
pub use_action: UseAction,
pub consumable: bool,
}
#[derive(Debug, Inspect, PartialEq)]
pub enum TileLayer {
Bg,
Mid,
Fg,
}
#[derive(Debug)]
pub enum UseAction {
PlaceBgTile { id: BgTileId },
@ -79,78 +42,17 @@ pub enum UseAction {
PlaceFgTile { id: FgTileId },
RemoveTile { layer: TileLayer },
}
#[derive(Debug, Inspect)]
pub struct ItemDb {
pub db: Vec<ItemDef>,
}
impl Default for ItemDb {
fn default() -> Self {
Self {
db: vec![
ItemDef {
name: String::from("Dirt Block"),
graphic_name: String::from("tiles/dirt"),
tex_rect: IntRect::default(),
use_action: UseAction::PlaceMidTile {
id: MidTileId::DIRT,
},
consumable: true,
},
ItemDef {
name: String::from("Torch"),
graphic_name: String::from("tiles/torch"),
tex_rect: IntRect::default(),
use_action: UseAction::PlaceMidTile {
id: MidTileId::TORCH,
},
consumable: true,
},
ItemDef {
name: String::from("Platform"),
graphic_name: String::from("tiles/platform"),
tex_rect: IntRect::default(),
use_action: UseAction::PlaceMidTile {
id: MidTileId::PLATFORM,
},
consumable: true,
},
ItemDef {
name: String::from("Wood Pick"),
graphic_name: String::from("items/woodpick"),
tex_rect: IntRect::default(),
use_action: UseAction::RemoveTile {
layer: TileLayer::Mid,
},
consumable: true,
},
ItemDef {
name: String::from("Panzerium"),
graphic_name: String::from("tiles/panzerium"),
tex_rect: IntRect::default(),
use_action: UseAction::PlaceMidTile {
id: MidTileId::PANZERIUM,
},
consumable: true,
},
ItemDef {
name: String::from("Stone wall"),
graphic_name: String::from("tiles/stoneback"),
tex_rect: IntRect::default(),
use_action: UseAction::PlaceBgTile {
id: BgTileId::STONE,
},
consumable: true,
},
],
}
loop {}
}
}
pub mod items {
use super::ItemId;
pub const DIRT_BLOCK: ItemId = 0;
pub const TORCH: ItemId = 1;
pub const PLATFORM: ItemId = 2;

View file

@ -1,19 +1,14 @@
use std::fmt::Debug;
use egui_inspect::derive::Inspect;
use num_traits::{Num, Signed};
use serde::{Deserialize, Serialize};
use crate::world::{TPosSc, TilePos};
pub type WPosSc = u32;
#[derive(Clone, Copy, Debug, Inspect)]
pub struct WorldPos {
pub x: WPosSc,
pub y: WPosSc,
}
/// Tile size in pixels
pub const TILE_SIZE: u8 = 32;
/// Pixels per meter.
@ -21,25 +16,17 @@ pub const PX_PER_M: f32 = TILE_SIZE as f32 * 2.;
/// Meters per pixel
pub const M_PER_PX: f32 = 1. / PX_PER_M;
pub const FPS_TARGET: u8 = 60;
pub fn px_per_frame_to_m_per_s(px_per_frame: f32) -> f32 {
let m_per_frame = px_per_frame / PX_PER_M;
m_per_frame * FPS_TARGET as f32
loop {}
}
pub fn px_per_frame_to_km_h(px_per_frame: f32) -> f32 {
px_per_frame_to_m_per_s(px_per_frame) * 3.6
loop {}
}
/// World extent in tiles. Roughly 50km*50km.
pub const WORLD_EXTENT: TPosSc = 100_000;
impl WorldPos {
pub fn tile_pos(&self) -> TilePos {
TilePos {
x: wp_to_tp(self.x),
y: wp_to_tp(self.y),
}
loop {}
}
/// Horizontal center of the world
pub const CENTER: WPosSc = (WORLD_EXTENT / 2) * TILE_SIZE as WPosSc;
@ -50,47 +37,20 @@ impl WorldPos {
x: Self::CENTER,
y: Self::SURFACE,
};
pub(crate) fn to_s2dc(self) -> s2dc::Vec2 {
s2dc::Vec2 {
x: self.x as i32,
y: self.y as i32,
}
loop {}
}
}
pub fn wp_to_tp(wp: WPosSc) -> TPosSc {
(wp / TILE_SIZE as WPosSc) as TPosSc
loop {}
}
// Get the offset required to center an object of `xw` width inside an object of `yw` width.
//
// For example, let's say `xw` (+) is 10 and we want to center it inside `yw` (-), which is 20
//
// ++++++++++ (x uncentered)
// -------------------- (y)
// ++++++++++ (x centered)
//
// In this case, we needed to add 5 to x to achieve centering.
// This is the offset that this function calculates.
//
// We can calulate it by subtracting `xw` from `yw` (10), and dividing it by 2.
pub fn center_offset<N: From<u8> + Copy + Signed>(xw: N, yw: N) -> N {
let diff = yw - xw;
diff / N::from(2)
loop {}
}
/// A smooth triangle-wave like transform of the input value, oscillating between 0 and the ceiling.
pub fn smoothwave<T: Num + From<u8> + PartialOrd + Copy>(input: T, max: T) -> T {
let period = max * T::from(2);
let value = input % period;
if value < max {
value
} else {
period - value
}
loop {}
}
#[derive(Serialize, Deserialize, Debug, Inspect, Default, Clone, Copy)]
pub struct IntRect {
pub x: i32,
@ -100,30 +60,14 @@ pub struct IntRect {
}
impl IntRect {
pub(crate) fn to_sf(self) -> sfml::graphics::Rect<i32> {
sfml::graphics::Rect::<i32> {
left: self.x,
top: self.y,
width: self.w,
height: self.h,
}
loop {}
}
}
#[test]
fn test_smooth_wave() {
assert_eq!(smoothwave(0, 100), 0);
assert_eq!(smoothwave(50, 100), 50);
assert_eq!(smoothwave(125, 100), 75);
assert_eq!(smoothwave(150, 100), 50);
assert_eq!(smoothwave(175, 100), 25);
assert_eq!(smoothwave(199, 100), 1);
assert_eq!(smoothwave(200, 100), 0);
assert_eq!(smoothwave(201, 100), 1);
loop {}
}
#[test]
fn test_wp_to_tp() {
assert_eq!(wp_to_tp(0), 0);
assert_eq!(wp_to_tp(1), 0);
assert_eq!(wp_to_tp(33), 1);
loop {}
}

View file

@ -1,14 +1,11 @@
use std::path::Path;
use egui_inspect::{derive::Inspect, inspect};
use s2dc::{vec2, MobileEntity};
use serde::{Deserialize, Serialize};
use crate::{
math::{WorldPos, TILE_SIZE},
world::{TPosSc, TilePos},
};
#[derive(Debug, Inspect, Serialize, Deserialize)]
pub struct Player {
#[inspect_with(inspect_mobile_entity)]
@ -19,43 +16,24 @@ pub struct Player {
/// true if the player wants to jump down from a platform
pub down_intent: bool,
}
fn inspect_mobile_entity(en: &mut MobileEntity, ui: &mut egui::Ui, _id_src: u64) {
inspect! {
ui,
en.en.pos.x,
en.en.pos.y,
en.en.bb.x,
en.en.bb.y
}
loop {}
}
impl Player {
pub fn new_at(pos: WorldPos) -> Self {
Self {
col_en: MobileEntity::from_pos_and_bb(vec2(pos.x as i32, pos.y as i32), vec2(20, 46)),
vspeed: 0.0,
hspeed: 0.0,
jumps_left: 0,
down_intent: false,
}
loop {}
}
#[allow(dead_code)]
pub fn center_tp(&self) -> TilePos {
TilePos {
x: (self.col_en.en.pos.x / TILE_SIZE as i32) as TPosSc,
y: (self.col_en.en.pos.y / TILE_SIZE as i32) as TPosSc,
}
loop {}
}
pub fn can_jump(&self) -> bool {
self.jumps_left > 0
loop {}
}
pub fn feet_y(&self) -> i32 {
self.col_en.en.pos.y + self.col_en.en.bb.y
loop {}
}
pub(crate) fn save(&self, path: &Path) {
let result = std::fs::write(path.join("player.dat"), rmp_serde::to_vec(self).unwrap());
log::info!("{result:?}");
loop {}
}
}

View file

@ -1,20 +1,13 @@
use sfml::audio::Music;
use crate::texture_atlas::AtlasBundle;
#[derive(Debug)]
pub struct Res {
pub atlas: AtlasBundle,
pub surf_music: Music<'static>,
pub und_music: Music<'static>,
}
impl Res {
pub fn load() -> anyhow::Result<Self> {
Ok(Self {
atlas: AtlasBundle::new()?,
surf_music: Music::from_file("res/music/music.ogg").unwrap(),
und_music: Music::from_file("res/music/cave2.ogg").unwrap(),
})
loop {}
}
}

View file

@ -1,22 +1,8 @@
use std::fmt;
use crate::math::M_PER_PX;
pub struct LengthDisp(pub f32);
impl fmt::Display for LengthDisp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let meters = self.0 * M_PER_PX;
if meters.abs() > 1000. {
let km = if meters.is_sign_negative() {
(meters / 1000.).ceil()
} else {
(meters / 1000.).floor()
};
let m = meters % 1000.;
write!(f, "{km} km, {m} m")
} else {
write!(f, "{meters} m")
}
loop {}
}
}

View file

@ -1,106 +1,28 @@
use std::{collections::HashMap, path::Path};
use crate::math::IntRect;
use sfml::{graphics::Texture, SfBox};
use texture_packer::{texture::Texture as _, TexturePacker, TexturePackerConfig};
pub type RectMap = HashMap<String, IntRect>;
#[derive(Debug)]
pub struct AtlasBundle {
pub tex: SfBox<Texture>,
// Key could be `tiles/dirt` for example, derived from folder+filename without extension
pub rects: RectMap,
}
impl AtlasBundle {
pub fn new() -> anyhow::Result<Self> {
let cfg = TexturePackerConfig {
max_width: 512,
max_height: 512,
allow_rotation: false,
border_padding: 0,
texture_padding: 0,
texture_extrusion: 0,
trim: true,
texture_outlines: false,
};
let mut packer = TexturePacker::new_skyline(cfg);
walk_graphics(|path| {
let img = image::open(path).unwrap();
let key = path_img_key(path);
packer.pack_own(key, img).unwrap();
dbg!(path);
});
let mut rects = HashMap::new();
let mut tex = Texture::new().unwrap();
log::info!(
"Texture atlas size is: {}x{}",
packer.width(),
packer.height()
);
if !tex.create(packer.width(), packer.height()) {
panic!("Failed to create texture");
}
let pixbuf = make_pix_buf(&packer);
unsafe {
tex.update_from_pixels(&pixbuf, packer.width(), packer.height(), 0, 0);
}
for (k, frame) in packer.get_frames() {
rects.insert(
k.clone(),
IntRect {
x: frame.frame.x as i32,
y: frame.frame.y as i32,
w: frame.frame.w as i32,
h: frame.frame.h as i32,
},
);
}
Ok(AtlasBundle { tex, rects })
loop {}
}
}
fn make_pix_buf(packer: &TexturePacker<image::DynamicImage, String>) -> Vec<u8> {
let (w, h) = (packer.width(), packer.height());
let px_size = 4;
let mut vec = vec![0; w as usize * h as usize * px_size as usize];
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * px_size) as usize;
if let Some(px) = packer.get(x, y) {
vec[idx..idx + px_size as usize].copy_from_slice(&px.0);
}
}
}
vec
loop {}
}
fn path_img_key(path: &Path) -> String {
let mut rev_iter = path.components().rev();
let fname = rev_iter.next().unwrap();
let folder = rev_iter.next().unwrap();
let fname: &Path = fname.as_ref();
let folder: &Path = folder.as_ref();
folder
.join(fname.file_stem().unwrap())
.display()
.to_string()
loop {}
}
#[test]
fn test_path_img_key() {
assert_eq!(
&path_img_key("/home/person/res/graphics/tiles/foo.png".as_ref()),
"tiles/foo"
);
loop {}
}
fn walk_graphics(mut f: impl FnMut(&Path)) {
for en in walkdir::WalkDir::new("res/graphics") {
let en = en.unwrap();
if en.file_type().is_file() {
f(en.path());
}
}
loop {}
}

View file

@ -1,12 +1,9 @@
use std::fmt::Debug;
use egui_inspect::{derive::Inspect, Inspect};
use crate::{
graphics::{ScreenSc, ScreenVec},
math::TILE_SIZE,
};
#[derive(Debug, Default, Inspect)]
pub struct TileDbEdit {
open: bool,
@ -15,109 +12,39 @@ pub struct TileDbEdit {
}
impl TileDbEdit {
pub(crate) fn ui(&mut self, ctx: &egui::Context, tile_db: &mut TileDb) {
if !self.open {
return;
}
egui::Window::new("Tiledb editor").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.selectable_value(&mut self.layer, Layer::Bg, "Bg");
ui.selectable_value(&mut self.layer, Layer::Mid, "Mid");
ui.selectable_value(&mut self.layer, Layer::Fg, "Fg");
});
ui.separator();
egui::ScrollArea::vertical()
.max_height(400.0)
.show(ui, |ui| match self.layer {
Layer::Bg => db_ui(&mut tile_db.bg, ui),
Layer::Fg => db_ui(&mut tile_db.fg, ui),
Layer::Mid => db_ui(&mut tile_db.mid, ui),
});
});
loop {}
}
}
#[derive(Debug, PartialEq, Eq)]
enum Layer {
Bg,
Fg,
Mid,
}
impl Default for Layer {
fn default() -> Self {
Self::Bg
loop {}
}
}
use super::{Bg, Fg, Mid, TileDb, TileDef, TileLayer, DEFAULT_TILE_BB};
trait SpecialUi {
fn special_ui(&mut self, ui: &mut egui::Ui);
}
impl SpecialUi for TileDef<Mid> {
fn special_ui(&mut self, ui: &mut egui::Ui) {
match &mut self.layer.bb {
Some(bb) => {
ui.horizontal(|ui| {
ui.label("x");
ui.add(egui::DragValue::new(&mut bb.x));
ui.label("y");
ui.add(egui::DragValue::new(&mut bb.y));
ui.label("w");
ui.add(egui::DragValue::new(&mut bb.w));
ui.label("h");
ui.add(egui::DragValue::new(&mut bb.h));
});
}
None => {
if ui.button("Insert bb").clicked() {
self.layer.bb = Some(DEFAULT_TILE_BB);
}
}
}
ui.checkbox(&mut self.layer.platform, "platform");
loop {}
}
}
impl SpecialUi for TileDef<Bg> {
fn special_ui(&mut self, _ui: &mut egui::Ui) {}
}
impl SpecialUi for TileDef<Fg> {
fn special_ui(&mut self, _ui: &mut egui::Ui) {}
}
fn db_ui<Layer: TileLayer + Debug>(db: &mut Vec<TileDef<Layer>>, ui: &mut egui::Ui)
where
<Layer as TileLayer>::SpecificDef: Debug + Default + Inspect,
TileDef<Layer>: SpecialUi,
{
for (i, def) in db.iter_mut().enumerate() {
ui.label(i.to_string());
ui.text_edit_singleline(&mut def.graphic_name);
match &mut def.light {
Some(light) => {
ui.horizontal(|ui| {
ui.label("x");
ui.add(egui::DragValue::new(&mut light.x));
ui.label("y");
ui.add(egui::DragValue::new(&mut light.y));
});
}
None => {
if ui.button("Insert light emit").clicked() {
def.light = Some(ScreenVec {
x: TILE_SIZE as ScreenSc / 2,
y: TILE_SIZE as ScreenSc / 2,
});
}
}
}
def.special_ui(ui);
ui.separator();
}
if ui.button("Add new default").clicked() {
db.push(Default::default());
}
loop {}
}

View file

@ -1,52 +1,30 @@
mod reg_chunk_existence;
mod serialization;
use std::{
fmt::Debug,
fs::File,
io::Seek,
path::{Path, PathBuf},
};
use std::{fmt::Debug, fs::File, io::Seek, path::{Path, PathBuf}};
use egui_inspect::derive::Inspect;
use fnv::FnvHashMap;
use serde::{Deserialize, Serialize};
use crate::{
math::WorldPos,
player::Player,
tiles::{BgTileId, FgTileId, MidTileId, TileId},
world::reg_chunk_existence::ExistenceBitset,
worldgen::Worldgen,
math::WorldPos, player::Player, tiles::{BgTileId, FgTileId, MidTileId, TileId},
world::reg_chunk_existence::ExistenceBitset, worldgen::Worldgen,
};
use self::serialization::save_chunk;
pub type ChkPosSc = u16;
#[derive(Hash, PartialEq, Eq, Debug, Clone, Copy, Inspect)]
pub struct ChunkPos {
pub x: ChkPosSc,
pub y: ChkPosSc,
}
impl ChunkPos {
/// Returns the region this chunk position belongs to
pub fn region(&self) -> (u8, u8) {
(
(self.x / REGION_CHUNK_EXTENT as ChkPosSc) as u8,
(self.y / REGION_CHUNK_EXTENT as ChkPosSc) as u8,
)
loop {}
}
/// Returns the local position in the region (0-7)
pub fn local(&self) -> (u8, u8) {
(
(self.x % REGION_CHUNK_EXTENT as ChkPosSc) as u8,
(self.y % REGION_CHUNK_EXTENT as ChkPosSc) as u8,
)
loop {}
}
}
#[derive(Debug, Inspect)]
pub struct World {
/// The currently loaded chunks
@ -59,220 +37,108 @@ pub struct World {
#[opaque]
pub path: PathBuf,
}
impl World {
pub fn new(spawn_point: WorldPos, name: &str, path: PathBuf) -> Self {
Self {
chunks: Default::default(),
ticks: Default::default(),
player: Player::new_at(spawn_point),
name: name.to_string(),
path,
}
loop {}
}
/// Get mutable access to the tile at `pos`.
///
/// Loads or generates the containing chunk if necessary.
pub fn tile_at_mut(&mut self, pos: TilePos, worldgen: &Worldgen) -> &mut Tile {
let (chk, local) = pos.to_chunk_and_local();
let chk = self
.chunks
.entry(chk)
.or_insert_with(|| Chunk::load_or_gen(chk, worldgen, &self.path));
chk.at_mut(local)
loop {}
}
pub fn save(&self) {
let result = std::fs::create_dir_all(&self.path);
log::info!("{result:?}");
self.save_meta();
self.player.save(&self.path);
self.save_chunks();
loop {}
}
pub fn save_meta(&self) {
let meta = WorldMetaSave {
name: self.name.clone(),
ticks: self.ticks,
};
let result = std::fs::write(
self.path.join("world.dat"),
rmp_serde::to_vec(&meta).unwrap(),
);
log::info!("{result:?}");
loop {}
}
pub fn save_chunks(&self) {
for (pos, chk) in self.chunks.iter() {
save_chunk(pos, chk, &self.path);
}
loop {}
}
}
fn loc_byte_idx_xy(x: u8, y: u8) -> usize {
loc_byte_idx(loc_idx(y, x))
loop {}
}
fn loc_byte_idx(loc_idx: u8) -> usize {
loc_idx as usize * CHUNK_BYTES
loop {}
}
fn loc_idx(loc_y: u8, loc_x: u8) -> u8 {
(loc_y * REGION_CHUNK_EXTENT) + loc_x
loop {}
}
fn format_reg_file_name((x, y): (u8, u8)) -> String {
format!("{x}.{y}.rgn")
loop {}
}
const CHUNK_BYTES: usize = CHUNK_N_TILES * TILE_BYTES;
const TILE_BYTES: usize = 3 * 2;
#[derive(Serialize, Deserialize)]
struct WorldMetaSave {
name: String,
ticks: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct TilePos {
pub x: TPosSc,
pub y: TPosSc,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct ChunkLocalTilePos {
pub x: ChkLocalTPosSc,
pub y: ChkLocalTPosSc,
}
/// Chunk-local tile position scalar. Supports up to 256 tiles per chunk.
type ChkLocalTPosSc = u8;
impl TilePos {
pub fn to_chunk_and_local(self) -> (ChunkPos, ChunkLocalTilePos) {
let chk = ChunkPos {
x: chk_pos(self.x),
y: chk_pos(self.y),
};
let local = ChunkLocalTilePos {
x: chunk_local(self.x),
y: chunk_local(self.y),
};
(chk, local)
loop {}
}
pub(crate) fn to_chunk(self) -> ChunkPos {
self.to_chunk_and_local().0
loop {}
}
}
fn chk_pos(tile: TPosSc) -> ChkPosSc {
(tile / CHUNK_EXTENT as TPosSc) as ChkPosSc
loop {}
}
#[test]
fn test_chk_pos() {
assert_eq!(chk_pos(0), 0);
assert_eq!(chk_pos(1), 0);
assert_eq!(chk_pos(127), 0);
assert_eq!(chk_pos(128), 1);
loop {}
}
fn chunk_local(global: TPosSc) -> ChkLocalTPosSc {
(global % CHUNK_EXTENT as TPosSc) as ChkLocalTPosSc
loop {}
}
#[test]
fn test_chunk_local() {
assert_eq!(chunk_local(0), 0);
loop {}
}
#[test]
fn test_to_chunk_and_local() {
assert_eq!(
TilePos { x: 0, y: 0 }.to_chunk_and_local(),
(ChunkPos { x: 0, y: 0 }, ChunkLocalTilePos { x: 0, y: 0 })
);
assert_eq!(
TilePos { x: 1, y: 1 }.to_chunk_and_local(),
(ChunkPos { x: 0, y: 0 }, ChunkLocalTilePos { x: 1, y: 1 })
);
loop {}
}
// Need to support at least 4 million tiles long
pub type TPosSc = u32;
pub const CHUNK_EXTENT: u16 = 128;
const CHUNK_N_TILES: usize = CHUNK_EXTENT as usize * CHUNK_EXTENT as usize;
type ChunkTiles = [Tile; CHUNK_N_TILES];
fn default_chunk_tiles() -> ChunkTiles {
[Tile {
bg: TileId::EMPTY,
mid: TileId::EMPTY,
fg: TileId::EMPTY,
}; CHUNK_N_TILES]
loop {}
}
#[derive(Debug, Inspect)]
pub struct Chunk {
tiles: ChunkTiles,
}
impl Chunk {
pub fn gen(pos: ChunkPos, worldgen: &Worldgen) -> Self {
let mut tiles = default_chunk_tiles();
let noise = worldgen.chunk_noise(pos);
if pos.y >= 156 {
for (i, t) in tiles.iter_mut().enumerate() {
let x = i % CHUNK_EXTENT as usize;
let y = i / CHUNK_EXTENT as usize;
let noise = noise[x][y];
*t = noise;
}
}
// Unbreakable layer at bottom
if pos.y > 798 {
for b in &mut tiles {
b.mid = MidTileId::UNBREAKANIUM;
}
}
Self { tiles }
loop {}
}
pub fn load_or_gen(chk: ChunkPos, worldgen: &Worldgen, world_path: &Path) -> Chunk {
log::info!("Loading chunk {chk:?} (reg: {:?})", chk.region());
let reg_filename = world_path.join(format_reg_file_name(chk.region()));
if chunk_exists(&reg_filename, chk) {
log::info!("Chunk exists, loading");
let mut f = File::open(&reg_filename).unwrap();
let bitset = ExistenceBitset::read_from_file(&mut f);
log::info!("Existence bitset: {bitset:?}");
assert_eq!(f.stream_position().unwrap(), 8);
let decomp_data = zstd::decode_all(f).unwrap();
assert_eq!(decomp_data.len(), REGION_BYTES);
let local_pos = chk.local();
Chunk::load_from_region(&decomp_data, local_pos.0, local_pos.1)
} else {
log::warn!("Chunk at {:?} doesn't exist, generating.", chk);
Chunk::gen(chk, worldgen)
}
loop {}
}
fn at_mut(&mut self, local: ChunkLocalTilePos) -> &mut Tile {
&mut self.tiles[CHUNK_EXTENT as usize * local.y as usize + local.x as usize]
loop {}
}
}
fn chunk_exists(reg_path: &Path, pos: ChunkPos) -> bool {
if !Path::new(&reg_path).exists() {
return false;
}
let bitset = ExistenceBitset::read_from_fs(reg_path);
let local = pos.local();
let idx = loc_idx(local.1, local.0);
crate::bitmanip::nth_bit_set(bitset.0, idx as usize)
loop {}
}
#[derive(Clone, Copy, Debug, Inspect)]
pub struct Tile {
/// Background wall behind entities
@ -282,12 +148,10 @@ pub struct Tile {
/// A layer on top of the mid wall. Usually ores or decorative pieces.
pub fg: FgTileId,
}
pub const REGION_CHUNK_EXTENT: u8 = 8;
pub const REGION_N_CHUNKS: u8 = REGION_CHUNK_EXTENT * REGION_CHUNK_EXTENT;
/// This is the uncompressed byte length of a region
pub const REGION_BYTES: usize = REGION_N_CHUNKS as usize * CHUNK_BYTES;
#[allow(clippy::assertions_on_constants)]
const _: () = assert!(
REGION_N_CHUNKS <= 64,

View file

@ -1,37 +1,17 @@
use std::{fs::File, io::Read, path::Path};
#[derive(Clone, Copy)]
pub struct ExistenceBitset(pub u64);
impl ExistenceBitset {
pub const EMPTY: Self = Self(0);
pub fn read_from_file(f: &mut File) -> ExistenceBitset {
let mut buf = [0; 8];
f.read_exact(&mut buf).unwrap();
ExistenceBitset(u64::from_le_bytes(buf))
loop {}
}
pub fn read_from_fs(path: &Path) -> ExistenceBitset {
let mut f = File::open(path).unwrap();
Self::read_from_file(&mut f)
loop {}
}
}
impl std::fmt::Debug for ExistenceBitset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f)?;
for i in 0..64 {
let chr = if crate::bitmanip::nth_bit_set(self.0, i) {
'X'
} else {
'_'
};
write!(f, "{chr}")?;
if (i + 1) % 8 == 0 {
writeln!(f)?;
}
}
Ok(())
loop {}
}
}

View file

@ -1,93 +1,22 @@
use std::{
fs::OpenOptions,
io::{Seek, Write},
fs::OpenOptions, io::{Seek, Write},
path::Path,
};
use crate::world::{
format_reg_file_name, loc_byte_idx, loc_idx, reg_chunk_existence::ExistenceBitset,
REGION_BYTES, TILE_BYTES,
};
use super::{default_chunk_tiles, loc_byte_idx_xy, Chunk, ChunkPos};
pub(super) fn save_chunk(pos: &ChunkPos, chk: &Chunk, world_dir: &Path) {
let reg_file_name = world_dir.join(format_reg_file_name(pos.region()));
let reg_file_exists = Path::new(&reg_file_name).exists();
if !reg_file_exists {
log::warn!("Region file doesn't exist. Going to create one.");
}
let mut f = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&reg_file_name)
.unwrap();
let mut existence_bitset = if reg_file_exists {
ExistenceBitset::read_from_file(&mut f)
} else {
ExistenceBitset::EMPTY
};
let mut region_tile_data = if reg_file_exists {
assert_eq!(f.stream_position().unwrap(), 8);
zstd::decode_all(&mut f).unwrap()
} else {
vec![0; REGION_BYTES]
};
// Even the zstd decompressed data should be exactly REGION_BYTES size
assert_eq!(region_tile_data.len(), REGION_BYTES);
let (loc_x, loc_y) = pos.local();
let loc_idx = loc_idx(loc_y, loc_x);
crate::bitmanip::set_nth_bit(&mut existence_bitset.0, loc_idx as usize, true);
let byte_idx = loc_byte_idx(loc_idx);
for (i, tile) in chk.tiles.iter().enumerate() {
let off = byte_idx + (i * TILE_BYTES);
region_tile_data[off..off + 2].copy_from_slice(&tile.bg.0.to_le_bytes());
region_tile_data[off + 2..off + 4].copy_from_slice(&tile.mid.0.to_le_bytes());
region_tile_data[off + 4..off + 6].copy_from_slice(&tile.fg.0.to_le_bytes());
}
f.rewind().unwrap();
f.write_all(&u64::to_le_bytes(existence_bitset.0)[..])
.unwrap();
assert_eq!(f.stream_position().unwrap(), 8);
assert_eq!(region_tile_data.len(), REGION_BYTES);
let result = f.write_all(&zstd::encode_all(&region_tile_data[..], COMP_LEVEL).unwrap());
let cursor = f.stream_position().unwrap();
f.set_len(cursor).unwrap();
log::info!("{result:?}");
loop {}
}
const COMP_LEVEL: i32 = 9;
impl Chunk {
pub fn load_from_region(data: &[u8], x: u8, y: u8) -> Self {
let byte_idx = loc_byte_idx_xy(x, y);
let mut tiles = default_chunk_tiles();
for (i, t) in tiles.iter_mut().enumerate() {
let off = byte_idx + (i * TILE_BYTES);
t.bg.0 = u16::from_le_bytes(data[off..off + 2].try_into().unwrap());
t.mid.0 = u16::from_le_bytes(data[off + 2..off + 4].try_into().unwrap());
t.fg.0 = u16::from_le_bytes(data[off + 4..off + 6].try_into().unwrap());
}
Self { tiles }
loop {}
}
}
#[test]
fn test_chunk_seri() {
env_logger::builder()
.filter_level(log::LevelFilter::Info)
.init();
let _ = std::fs::create_dir("testworld");
let mut chk = Chunk {
tiles: super::default_chunk_tiles(),
};
for t in &mut chk.tiles {
t.bg = crate::tiles::BgTileId::DIRT;
}
save_chunk(&ChunkPos { x: 2, y: 0 }, &chk, "testworld".as_ref());
save_chunk(&ChunkPos { x: 3, y: 0 }, &chk, "testworld".as_ref());
let raw = std::fs::read("testworld/0.0.rgn").unwrap();
zstd::decode_all(&raw[8..]).unwrap();
std::fs::remove_dir_all("testworld").unwrap();
loop {}
}

View file

@ -1,74 +1,23 @@
use worldgen::{
constraint,
noise::perlin::PerlinNoise,
constraint, noise::perlin::PerlinNoise,
noisemap::{NoiseMap, NoiseMapGenerator, Seed, Step},
world::{
tile::{Constraint, ConstraintType},
Size, Tile, World,
},
};
use crate::{
tiles::{BgTileId, FgTileId, MidTileId, TileId},
world::{ChunkPos, Tile as Tl, CHUNK_EXTENT},
};
pub struct Worldgen {
world: World<crate::world::Tile>,
}
impl Worldgen {
pub fn from_seed(seed: i64) -> Self {
let noise = PerlinNoise::new();
let nm1 = NoiseMap::new(noise)
.set(Seed::of(seed))
.set(Step::of(0.005, 0.005));
let nm2 = NoiseMap::new(noise)
.set(Seed::of(seed))
.set(Step::of(0.05, 0.05));
let nm = Box::new(nm1 + nm2 * 3);
let world = World::new()
.set(Size::of(CHUNK_EXTENT as i64, CHUNK_EXTENT as i64))
// Dirt coal
.add(
Tile::new(Tl {
bg: BgTileId::DIRT,
mid: MidTileId::DIRT,
fg: FgTileId::COAL,
})
.when(constraint!(nm.clone(), < -0.8)),
)
// Dirt
.add(
Tile::new(Tl {
bg: BgTileId::DIRT,
mid: MidTileId::DIRT,
fg: TileId::EMPTY,
})
.when(constraint!(nm.clone(), < -0.1)),
)
// Stone
.add(
Tile::new(Tl {
bg: BgTileId::STONE,
mid: MidTileId::STONE,
fg: TileId::EMPTY,
})
.when(constraint!(nm, < 0.45)),
)
// Dirt wall
.add(Tile::new(Tl {
bg: BgTileId::DIRT,
mid: TileId::EMPTY,
fg: TileId::EMPTY,
}));
Self { world }
loop {}
}
pub fn chunk_noise(&self, pos: ChunkPos) -> Vec<Vec<Tl>> {
self.world.generate(pos.x as i64, pos.y as i64).unwrap()
loop {}
}
}