Initial commit

This commit is contained in:
Alex Page 2022-01-19 22:07:59 -05:00
commit c66d65d8af
7 changed files with 3024 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.vscode

2465
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

13
Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "kroz"
version = "0.1.0"
edition = "2021"
[dependencies]
bracket-lib = { git = "https://github.com/amethyst/bracket-lib" }
specs = "0.16.1"
specs-derive = "0.4.1"
cpal = "0.13"
oddio = "0.5"
rand = "0.8"
spin_sleep = "1.0"

286
src/main.rs Normal file
View file

@ -0,0 +1,286 @@
#![windows_subsystem = "windows"]
use bracket_lib::prelude::*;
use sidebar::draw_sidebar;
use sound::{Sound, SoundEffect, SoundEffectSamples, SoundSystem};
use specs::prelude::*;
use specs_derive::Component;
use std::{
cmp::{max, min},
time::{Duration, Instant},
};
use vga_color::*;
use crate::sound::SoundType;
mod sidebar;
mod sound;
pub mod vga_color;
#[derive(Default)]
struct LevelNumber(u32);
struct SoundEffects {
step_sound: SoundEffectSamples,
pickup_sound: SoundEffectSamples,
}
#[derive(Component)]
struct Position {
x: i32,
y: i32,
}
#[derive(Component)]
struct Renderable {
glyph: FontCharType,
fg: RGB,
bg: RGB,
}
#[derive(Component)]
struct LeftMover {}
#[derive(Component, Debug)]
struct Player {
last_moved: Instant,
score: u32,
gems: u32,
whips: u32,
teleports: u32,
keys: u32,
}
struct State {
ecs: World,
sound_system: SoundSystem,
}
fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World, sound_system: &mut SoundSystem) {
let mut positions = ecs.write_storage::<Position>();
let mut players = ecs.write_storage::<Player>();
let mut stepped = false;
for (player, pos) in (&mut players, &mut positions).join() {
let now = Instant::now();
if now - player.last_moved > Duration::from_secs_f32(1. / 7.5) {
pos.x = min(65, max(0, pos.x + delta_x));
pos.y = min(24, max(0, pos.y + delta_y));
stepped = true;
player.last_moved = now;
}
}
if stepped {
let sound_effects = ecs.fetch::<SoundEffects>();
let step_sound = sound_effects.step_sound.clone();
let _ = sound_system.play_sound(step_sound);
}
}
fn player_input(gs: &mut State, ctx: &mut BTerm) {
// Player movement
match ctx.key {
None => {} // Nothing happened
Some(key) => match key {
VirtualKeyCode::Left | VirtualKeyCode::J => {
try_move_player(-1, 0, &mut gs.ecs, &mut gs.sound_system)
}
VirtualKeyCode::U => try_move_player(-1, -1, &mut gs.ecs, &mut gs.sound_system),
VirtualKeyCode::Up | VirtualKeyCode::I => {
try_move_player(0, -1, &mut gs.ecs, &mut gs.sound_system)
}
VirtualKeyCode::O => try_move_player(1, -1, &mut gs.ecs, &mut gs.sound_system),
VirtualKeyCode::Right | VirtualKeyCode::K => {
try_move_player(1, 0, &mut gs.ecs, &mut gs.sound_system)
}
VirtualKeyCode::Comma => try_move_player(1, 1, &mut gs.ecs, &mut gs.sound_system),
VirtualKeyCode::Down | VirtualKeyCode::M => {
try_move_player(0, 1, &mut gs.ecs, &mut gs.sound_system)
}
VirtualKeyCode::N => try_move_player(-1, 1, &mut gs.ecs, &mut gs.sound_system),
VirtualKeyCode::S => {
let sound_effects = gs.ecs.fetch::<SoundEffects>();
let step_sound = sound_effects.pickup_sound.clone();
let _ = gs.sound_system.play_sound(step_sound);
}
_ => {}
},
}
}
impl GameState for State {
fn tick(&mut self, ctx: &mut BTerm) {
ctx.cls();
player_input(self, ctx);
self.run_systems();
let positions = self.ecs.read_storage::<Position>();
let renderables = self.ecs.read_storage::<Renderable>();
for (pos, render) in (&positions, &renderables).join() {
ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph);
}
draw_sidebar(&self.ecs, ctx);
}
}
struct LeftWalker {}
impl<'a> System<'a> for LeftWalker {
type SystemData = (ReadStorage<'a, LeftMover>, WriteStorage<'a, Position>);
fn run(&mut self, (lefty, mut pos): Self::SystemData) {
for (_lefty, pos) in (&lefty, &mut pos).join() {
pos.x -= 1;
if pos.x < 0 {
pos.x = 65;
}
}
}
}
impl State {
fn run_systems(&mut self) {
let mut lw = LeftWalker {};
lw.run_now(&self.ecs);
self.ecs.maintain();
}
}
fn main() -> BError {
let context = BTermBuilder::simple(80, 25)?
.with_fps_cap(60.0)
.with_title("Kroz")
.with_tile_dimensions(8, 16)
.build()?;
let ss = SoundSystem::new();
let step_sound = ss.render_sound_effect(SoundEffect {
sounds: vec![
Sound {
sound_type: SoundType::Noise(350, 900),
duration: Duration::from_millis(6),
},
Sound {
sound_type: SoundType::Silence,
duration: Duration::from_millis(120),
},
Sound {
sound_type: SoundType::Noise(150, 200),
duration: Duration::from_millis(6),
},
],
});
let pickup_sound = ss.render_sound_effect(SoundEffect {
sounds: vec![
Sound {
sound_type: SoundType::Noise(350, 900),
duration: Duration::from_millis(6),
},
Sound {
sound_type: SoundType::Silence,
duration: Duration::from_millis(120),
},
Sound {
sound_type: SoundType::Noise(1000, 2000),
duration: Duration::from_millis(20),
},
],
});
let mut gs = State {
ecs: World::new(),
// sound_sender: tx,
sound_system: ss,
//step_sound,
};
gs.ecs.insert(LevelNumber(0));
gs.ecs.insert(SoundEffects {
step_sound,
pickup_sound,
});
gs.ecs.register::<Position>();
gs.ecs.register::<Renderable>();
gs.ecs.register::<LeftMover>();
gs.ecs.register::<Player>();
gs.ecs
.create_entity()
.with(Position { x: 40, y: 24 })
.with(Renderable {
glyph: to_cp437('☻'),
fg: RGB::named(VGA_YELLOW_BRIGHT),
bg: RGB::named(VGA_BLACK),
})
.with(Player {
last_moved: Instant::now(),
score: 1290,
gems: 14,
whips: 7,
teleports: 0,
keys: 0,
})
.build();
for i in 0..10 {
gs.ecs
.create_entity()
.with(Position { x: i * 7, y: 20 })
.with(Renderable {
glyph: to_cp437('Ä'),
fg: RGB::named(VGA_RED_BRIGHT),
bg: RGB::named(VGA_BLACK),
})
.with(LeftMover {})
.build();
}
// let descent_sounds: Vec<Sound> = (20..100)
// .rev()
// .flat_map(|x| {
// (1..10).rev().flat_map(move |y| {
// vec![Sound {
// sound_type: SoundType::Tone(x * y * y),
// duration: Duration::from_millis((y as f64 / 1.5) as u64),
// }]
// })
// })
// .collect();
// let descent_effect = gs.sound_system.render_sound_effect(SoundEffect {
// sounds: descent_sounds,
// });
// let _ = gs.sound_system.play_sound(descent_effect);
let start_sounds: Vec<Sound> = (30..400)
.step_by(8)
.map(|x| Sound {
sound_type: SoundType::Tone(x),
duration: Duration::from_millis(24),
})
.collect();
let swoop_effect = gs.sound_system.render_sound_effect(SoundEffect {
sounds: start_sounds,
});
let _ = gs.sound_system.play_sound(swoop_effect);
// let effect = gs.sound_system.render_sound_effect(SoundEffect {
// sounds: vec![Sound {
// sound_type: SoundType::Tone(3500),
// duration: Duration::from_millis(4000),
// }],
// });
// let _ = gs.sound_system.play_sound(effect);
main_loop(context, gs)
}

106
src/sidebar.rs Normal file
View file

@ -0,0 +1,106 @@
use crate::{vga_color::*, LevelNumber, Player};
use bracket_lib::prelude::*;
use specs::prelude::*;
const SIDEBAR_POS_X: i32 = 66;
const SIDEBAR_POS_Y: i32 = 0;
pub fn draw_sidebar(ecs: &World, ctx: &mut BTerm) {
// Blue background
ctx.fill_region(
Rect {
x1: SIDEBAR_POS_X,
x2: SIDEBAR_POS_X + 14,
y1: SIDEBAR_POS_Y,
y2: SIDEBAR_POS_Y + 19,
},
to_cp437(' '),
RGB::named(VGA_YELLOW_BRIGHT),
RGB::named(VGA_BLUE),
);
// Gray number boxes
(1..17).step_by(3).for_each(|y| {
ctx.fill_region(
Rect {
x1: SIDEBAR_POS_X + 3,
x2: SIDEBAR_POS_X + 10,
y1: SIDEBAR_POS_Y + y,
y2: SIDEBAR_POS_Y + y + 1,
},
to_cp437(' '),
RGB::named(VGA_RED),
RGB::named(VGA_WHITE),
);
});
// Stats
ctx.print(SIDEBAR_POS_X + 4, SIDEBAR_POS_Y, "Score");
ctx.print(SIDEBAR_POS_X + 4, SIDEBAR_POS_Y + 3, "Level");
ctx.print_centered_at(
SIDEBAR_POS_X + 6,
SIDEBAR_POS_Y + 4,
ecs.read_resource::<LevelNumber>().0,
);
ctx.print(SIDEBAR_POS_X + 4, SIDEBAR_POS_Y + 6, "Gems");
ctx.print(SIDEBAR_POS_X + 4, SIDEBAR_POS_Y + 9, "Whips");
ctx.print(SIDEBAR_POS_X + 2, SIDEBAR_POS_Y + 12, "Teleports");
ctx.print(SIDEBAR_POS_X + 4, SIDEBAR_POS_Y + 15, "Keys");
let players = ecs.read_storage::<Player>();
for player in players.join() {
ctx.print_centered_at(SIDEBAR_POS_X + 6, SIDEBAR_POS_Y + 1, player.score);
ctx.print_centered_at(SIDEBAR_POS_X + 6, SIDEBAR_POS_Y + 7, player.gems);
ctx.print_centered_at(SIDEBAR_POS_X + 6, SIDEBAR_POS_Y + 10, player.whips);
ctx.print_centered_at(SIDEBAR_POS_X + 6, SIDEBAR_POS_Y + 13, player.teleports);
ctx.print_centered_at(SIDEBAR_POS_X + 6, SIDEBAR_POS_Y + 16, player.keys);
}
// Hotkey list
ctx.print_color(
SIDEBAR_POS_X + 3,
SIDEBAR_POS_Y + 18,
RGB::named(VGA_CYAN_BRIGHT),
RGB::named(VGA_RED),
"OPTIONS",
);
ctx.fill_region(
Rect {
x1: SIDEBAR_POS_X,
x2: SIDEBAR_POS_X + 14,
y1: SIDEBAR_POS_Y + 19,
y2: SIDEBAR_POS_Y + 25,
},
to_cp437(' '),
RGB::named(VGA_WHITE),
RGB::named(VGA_BLUE),
);
ctx.fill_region(
Rect {
x1: SIDEBAR_POS_X + 3,
x2: SIDEBAR_POS_X + 4,
y1: SIDEBAR_POS_Y + 19,
y2: SIDEBAR_POS_Y + 25,
},
to_cp437(' '),
RGB::named(VGA_WHITE_BRIGHT),
RGB::named(VGA_BLUE),
);
ctx.print(SIDEBAR_POS_X + 3, SIDEBAR_POS_Y + 19, "Whip");
ctx.print(SIDEBAR_POS_X + 3, SIDEBAR_POS_Y + 20, "Teleport");
ctx.print(SIDEBAR_POS_X + 3, SIDEBAR_POS_Y + 21, "Pause");
ctx.print(SIDEBAR_POS_X + 3, SIDEBAR_POS_Y + 22, "Quit");
ctx.print(SIDEBAR_POS_X + 3, SIDEBAR_POS_Y + 23, "Save");
ctx.print(SIDEBAR_POS_X + 3, SIDEBAR_POS_Y + 24, "Restore");
ctx.print_color_right(
SIDEBAR_POS_X + 14,
SIDEBAR_POS_Y,
RGB::named(VGA_GREEN_BRIGHT),
RGB::named(VGA_BLACK),
&format!("{}", ctx.fps),
);
}

133
src/sound.rs Normal file
View file

@ -0,0 +1,133 @@
use std::{f32::consts::PI, sync::Arc, time::Duration};
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
SampleRate, Stream,
};
use oddio::{Frames, Handle, Mixer};
use rand::Rng;
type Frequency = u32;
type MinFrequency = u32;
type MaxFrequency = u32;
pub type SoundEffectSamples = Arc<Frames<f32>>;
pub enum SoundType {
Silence,
Tone(Frequency),
Noise(MinFrequency, MaxFrequency),
}
pub struct Sound {
pub sound_type: SoundType,
pub duration: Duration,
}
pub struct SoundEffect {
pub sounds: Vec<Sound>,
}
pub struct SoundSystem {
mixer_handle: Handle<Mixer<[f32; 2]>>,
sample_rate: SampleRate,
_stream: Stream,
}
impl SoundSystem {
pub fn new() -> Self {
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("no output device available");
let sample_rate = device.default_output_config().unwrap().sample_rate();
let config = cpal::StreamConfig {
channels: 2,
sample_rate,
buffer_size: cpal::BufferSize::Default,
};
let (mixer_handle, mixer) = oddio::split(oddio::Mixer::new());
let stream = device
.build_output_stream(
&config,
move |out_flat: &mut [f32], _: &cpal::OutputCallbackInfo| {
let out_stereo: &mut [[f32; 2]] = oddio::frame_stereo(out_flat);
oddio::run(&mixer, sample_rate.0, out_stereo);
},
move |err| {
eprintln!("{}", err);
},
)
.unwrap();
stream.play().unwrap();
Self {
mixer_handle,
sample_rate,
_stream: stream,
}
}
pub fn render_sound_effect(&self, effect: SoundEffect) -> SoundEffectSamples {
let effect_buffer: Vec<f32> = effect
.sounds
.iter()
.flat_map(|sound| match sound.sound_type {
SoundType::Silence => (0
..(self.sample_rate.0 as f32 * sound.duration.as_secs_f32()) as usize)
.map(|_| 0f32)
.collect::<Vec<f32>>(),
SoundType::Tone(freq) => {
if freq == 0 {
return (0..(self.sample_rate.0 as f32 * sound.duration.as_secs_f32())
as usize)
.map(|_| 0f32)
.collect::<Vec<f32>>();
}
let num_harmonics = self.sample_rate.0 / (freq as u32 * 2);
let coefficients = (0..num_harmonics + 1)
.map(|i| {
if i == 0 {
return 0.0;
}
(i as f32 * 0.5 * PI).sin() * 2.0 / (i as f32 * PI)
})
.collect::<Vec<f32>>();
let scaler = freq as f32 * PI * 2.0 / self.sample_rate.0 as f32;
(0..(self.sample_rate.0 as f32 * sound.duration.as_secs_f32()) as usize)
.map(|i| {
let temp = scaler * i as f32;
coefficients
.iter()
.enumerate()
.map(|(j, coef)| coef * (j as f32 * temp).cos())
.sum::<f32>()
})
.collect::<Vec<f32>>()
}
SoundType::Noise(min, max) => {
(0..(self.sample_rate.0 as f32 * sound.duration.as_secs_f32()) as usize)
.map(|i| {
let t = i as f32 / self.sample_rate.0 as f32;
(t * (rand::thread_rng().gen_range(min as f32..max as f32))
* 2.0
* std::f32::consts::PI)
.sin()
.signum()
})
.collect::<Vec<f32>>()
}
})
.collect();
oddio::Frames::from_iter(self.sample_rate.0, effect_buffer.iter().copied())
}
pub fn play_sound(&mut self, samples: SoundEffectSamples) {
self.mixer_handle
.control::<oddio::Mixer<_>, _>()
.play(oddio::MonoToStereo::new(oddio::FramesSignal::from(samples)));
}
}

19
src/vga_color.rs Normal file
View file

@ -0,0 +1,19 @@
// VGA 4-bit Colors
pub const VGA_BLACK: (u8, u8, u8) = (0, 0, 0);
pub const VGA_RED: (u8, u8, u8) = (170, 0, 0);
pub const VGA_GREEN: (u8, u8, u8) = (0, 170, 0);
pub const VGA_YELLOW: (u8, u8, u8) = (170, 85, 0);
pub const VGA_BLUE: (u8, u8, u8) = (0, 0, 170);
pub const VGA_MAGENTA: (u8, u8, u8) = (170, 0, 170);
pub const VGA_CYAN: (u8, u8, u8) = (0, 170, 170);
pub const VGA_WHITE: (u8, u8, u8) = (170, 170, 170);
// "Bold" VGA 4-bit Colors
pub const VGA_BLACK_BRIGHT: (u8, u8, u8) = (85, 85, 85);
pub const VGA_RED_BRIGHT: (u8, u8, u8) = (255, 85, 85);
pub const VGA_GREEN_BRIGHT: (u8, u8, u8) = (85, 255, 85);
pub const VGA_YELLOW_BRIGHT: (u8, u8, u8) = (255, 255, 85);
pub const VGA_BLUE_BRIGHT: (u8, u8, u8) = (85, 85, 255);
pub const VGA_MAGENTA_BRIGHT: (u8, u8, u8) = (255, 85, 255);
pub const VGA_CYAN_BRIGHT: (u8, u8, u8) = (85, 255, 255);
pub const VGA_WHITE_BRIGHT: (u8, u8, u8) = (255, 255, 255);