Commit e84900dd authored by neosam's avatar neosam
Browse files

Merge branch 'rahix/wip' into 'master'

Implement bottle angel shift (and the underlying shift management code)

Closes #12

See merge request !29
parents 8f824eb0 3f22c558
use crate::colliders;
use crate::components;
use crate::entities;
use crate::resources;
use crate::svg_loader;
pub struct BottleAngelShift {
hours: usize,
level: String,
}
impl BottleAngelShift {
pub fn generate(rng: &mut impl rand::Rng) -> BottleAngelShift {
BottleAngelShift {
hours: rng.gen_range(1, 3),
// TODO: When there are more levels, randomly select one here.
level: "assembly-hall-1.svg".to_owned(),
}
}
}
impl super::AngelShiftImpl for BottleAngelShift {
fn metadata(&self) -> super::ShiftMetadata {
super::ShiftMetadata {
title: "Bottle Angel".to_owned(),
description: "Collect bottles from all bottle drop points in the designated area."
.to_owned(),
hours: self.hours,
}
}
fn level_name(&self) -> &str {
&self.level
}
fn init_gameworld(
&self,
world: &mut legion::World,
resources: &mut legion::Resources,
schedule_builder: &mut legion::systems::Builder,
level: &svg_loader::SvgLevel,
) {
entities::create_drop_points(world, level);
resources.insert(BottleAngelState::new(4));
schedule_builder
.add_thread_local(collect_bottledrops_system())
.add_thread_local(update_bottle_shift_system(self.hours));
}
}
pub struct BottleAngelState {
collected_drops: usize,
drops_in_map: usize,
}
impl BottleAngelState {
fn new(drops_in_map: usize) -> BottleAngelState {
BottleAngelState {
collected_drops: 0,
drops_in_map,
}
}
}
#[legion::system]
#[read_component(colliders::Collider)]
#[read_component(components::Matebottledrop)]
pub fn collect_bottledrops(
world: &legion::world::SubWorld,
cmd: &mut legion::systems::CommandBuffer,
#[resource] player: &resources::Player,
#[resource] collision_world: &colliders::CollisionWorld,
#[resource] bottle_angel_state: &mut BottleAngelState,
) {
use legion::IntoQuery;
let collider = <&colliders::Collider>::query()
.get(world, player.0)
.unwrap();
let mut bottledrops = <&components::Matebottledrop>::query();
for pair in collision_world
.world
.proximities_with(collider.handle.unwrap(), false)
.unwrap()
{
if pair.3 != ncollide2d::query::Proximity::Intersecting {
continue;
}
let entity = *collision_world.world.objects.get(pair.1).unwrap().data();
if bottledrops.get(world, entity).is_ok() {
cmd.remove(entity);
bottle_angel_state.collected_drops += 1;
}
}
}
#[legion::system]
#[write_component(components::Player)]
pub fn update_bottle_shift(
#[state] hours_to_award: &usize,
world: &mut legion::world::SubWorld,
#[resource] player: &mut resources::Player,
#[resource] bottle_angel_state: &mut BottleAngelState,
#[resource] game_manager: &mut resources::GameManager,
) {
use legion::IntoQuery;
if bottle_angel_state.collected_drops >= bottle_angel_state.drops_in_map {
let player = <&mut components::Player>::query()
.get_mut(world, player.0)
.unwrap();
player.collected_hours += *hours_to_award as u32;
game_manager.request_return_to_heaven();
}
}
use crate::svg_loader;
pub trait AngelShiftImpl {
/// Return metadata about this shift.
///
/// Used in heaven to display what task is up next.
fn metadata(&self) -> ShiftMetadata;
/// Return name of the selected level for this shift.
fn level_name(&self) -> &str;
/// Initialize the shift-specific bits of the game-world
///
/// The world will already contain all common entities and resources. The schedule is
/// configured to run all "game world" systems like collision detection/movement before
/// whatever is added here. After the systems added here, rendering will be scheduled.
fn init_gameworld(
&self,
world: &mut legion::World,
resources: &mut legion::Resources,
schedule_builder: &mut legion::systems::Builder,
level: &svg_loader::SvgLevel,
);
}
pub struct ShiftMetadata {
/// Short name of this shift/task.
pub title: String,
/// Longer description of what needs to be done.
pub description: String,
/// Amount of hours that would be gained from working this shift.
pub hours: usize,
}
pub struct AngelShift(pub Box<dyn AngelShiftImpl>);
impl AngelShift {
pub fn metadata(&self) -> ShiftMetadata {
self.0.metadata()
}
pub fn level_name(&self) -> &str {
self.0.level_name()
}
pub fn init_gameworld(
&self,
world: &mut legion::World,
resources: &mut legion::Resources,
schedule_builder: &mut legion::systems::Builder,
level: &svg_loader::SvgLevel,
) {
self.0
.init_gameworld(world, resources, schedule_builder, level);
}
}
pub mod bottle_angel;
mod definitions;
pub use definitions::AngelShift;
pub use definitions::AngelShiftImpl;
pub use definitions::ShiftMetadata;
pub fn generate_random_shift(rng: &mut impl rand::Rng) -> AngelShift {
AngelShift(match rng.gen_range(0usize, 1) {
0 => Box::new(bottle_angel::BottleAngelShift::generate(rng)),
_ => unreachable!(),
})
}
#[derive(Debug, Clone)]
pub struct Player {
pub sanity: f32,
pub collected_hours: u32,
}
impl Player {
pub fn new() -> Player {
Player { sanity: 0.6 }
Player {
sanity: 1.0,
collected_hours: 0,
}
}
}
......@@ -10,6 +10,7 @@ pub fn create_player(
world: &mut legion::World,
resources: &legion::Resources,
level: &svg_loader::SvgLevel,
player: components::Player,
) -> legion::Entity {
let spawn_locations = level
.spawnpoints
......@@ -21,7 +22,7 @@ pub fn create_player(
let center = rendering.get_image_size(&image).unwrap() / 2.0;
world.push((
components::Player::new(),
player,
components::Position::new(spawn.x, spawn.y),
components::Movable::new(),
colliders::Collider::new_player(50.0),
......
use wasm_bindgen::prelude::*;
pub mod angel_shifts;
pub mod colliders;
pub mod colors;
pub mod components;
......
pub struct GameManager {
return_to_heaven: bool,
}
impl GameManager {
pub fn new() -> GameManager {
GameManager {
return_to_heaven: false,
}
}
pub fn request_return_to_heaven(&mut self) {
self.return_to_heaven = true;
}
pub fn wants_return_to_heaven(&self) -> bool {
self.return_to_heaven
}
}
mod camera;
mod clock;
mod game_manager;
mod player;
mod rendering;
pub use camera::Camera;
pub use clock::Clock;
pub use game_manager::GameManager;
pub use player::Player;
pub use rendering::Color;
pub use rendering::ImageHandle;
......
use crate::angel_shifts;
use crate::colors;
use crate::components;
use crate::gamestate;
......@@ -7,73 +8,54 @@ use crate::utils;
pub struct HeavenState {
gui_svg: web_sys::SvgElement,
node_world: legion::World,
resources: legion::Resources,
node_schedule: legion::Schedule,
player: components::Player,
assigned_shift: Option<angel_shifts::AngelShift>,
}
impl HeavenState {
pub fn new() -> HeavenState {
let mut resources = legion::Resources::default();
resources.insert(resources::Clock::new());
resources.insert(resources::Rendering::new("game-canvas").unwrap());
pub fn new(player: Option<components::Player>) -> HeavenState {
let (node_world, resources, node_schedule) = init_orbiting_nodes();
let mut node_world = legion::World::default();
let n0 = node_world.push((
components::Node::new(),
components::Position::new(800.0, 340.0),
components::OrbitBody::new(5.0, 10.0),
components::Gravity,
));
let n1 = node_world.push((
components::Node::new(),
components::Position::new(120.0, 300.0),
components::OrbitBody::new(5.0, -5.0),
components::Gravity,
));
let n2 = node_world.push((
components::Node::new(),
components::Position::new(700.0, 740.0),
components::OrbitBody::new(8.0, -4.0),
components::Gravity,
));
let n3 = node_world.push((
components::Node::new(),
components::Position::new(340.0, 290.0),
components::OrbitBody::new(10.0, 0.0),
components::Gravity,
));
let n4 = node_world.push((
components::Node::new(),
components::Position::new(300.0, 400.0),
components::OrbitBody::new(0.0, 10.0),
components::Gravity,
));
let player = player.unwrap_or_else(|| {
// Initialize the game as this is the first run.
components::Player::new()
});
node_world.push((components::Edge::new(n0, n1),));
node_world.push((components::Edge::new(n1, n2),));
node_world.push((components::Edge::new(n0, n2),));
node_world.push((components::Edge::new(n2, n3),));
node_world.push((components::Edge::new(n3, n4),));
node_world.push((components::Edge::new(n0, n4),));
// Update the angel stats GUI elements
utils::get_element_by_id::<web_sys::Element>("heaven-collected-hours")
.unwrap()
.set_inner_html(&format!("{}", player.collected_hours));
utils::get_element_by_id::<web_sys::Element>("heaven-needed-hours")
.unwrap()
.set_inner_html(&format!("{}", 30 - player.collected_hours));
node_world.push((components::TheSun, components::Position::new(600.0, 540.0)));
// Assign a random shift
let assigned_shift = angel_shifts::generate_random_shift(&mut rand::thread_rng());
let node_schedule = legion::Schedule::builder()
.add_system(components::update_gravity_system())
.add_system(components::update_movement_system())
.add_thread_local(components::update_nodes_system())
.flush()
.add_thread_local(components::draw_edges_system())
.add_thread_local(components::draw_nodes_system())
.add_thread_local(components::draw_thesun_system())
.build();
// Display the shift info
let shift_meta = assigned_shift.metadata();
utils::get_element_by_id::<web_sys::Element>("heaven-shift-title")
.unwrap()
.set_inner_html(&shift_meta.title);
utils::get_element_by_id::<web_sys::Element>("heaven-shift-description")
.unwrap()
.set_inner_html(&shift_meta.description);
utils::get_element_by_id::<web_sys::Element>("heaven-shift-hours")
.unwrap()
.set_inner_html(&format!("{}", shift_meta.hours));
HeavenState {
gui_svg: utils::get_element_by_id("heaven-ui").unwrap(),
node_world,
resources,
node_schedule,
player,
assigned_shift: Some(assigned_shift),
}
}
}
......@@ -113,7 +95,10 @@ impl gamestate::State for HeavenState {
gamestate::Event::MouseClick {
target: "heaven-start-shift",
..
} => gamestate::Transition::push(states::LevelLoadingState::new()),
} => gamestate::Transition::replace(states::LevelLoadingState::new(
self.player.clone(),
self.assigned_shift.take().unwrap(),
)),
event => {
crate::console_log!("unknown event {:?}", event);
gamestate::Transition::Keep
......@@ -121,3 +106,62 @@ impl gamestate::State for HeavenState {
}
}
}
fn init_orbiting_nodes() -> (legion::World, legion::Resources, legion::Schedule) {
let mut resources = legion::Resources::default();
resources.insert(resources::Clock::new());
resources.insert(resources::Rendering::new("game-canvas").unwrap());
let mut node_world = legion::World::default();
let n0 = node_world.push((
components::Node::new(),
components::Position::new(800.0, 340.0),
components::OrbitBody::new(5.0, 10.0),
components::Gravity,
));
let n1 = node_world.push((
components::Node::new(),
components::Position::new(120.0, 300.0),
components::OrbitBody::new(5.0, -5.0),
components::Gravity,
));
let n2 = node_world.push((
components::Node::new(),
components::Position::new(700.0, 740.0),
components::OrbitBody::new(8.0, -4.0),
components::Gravity,
));
let n3 = node_world.push((
components::Node::new(),
components::Position::new(340.0, 290.0),
components::OrbitBody::new(10.0, 0.0),
components::Gravity,
));
let n4 = node_world.push((
components::Node::new(),
components::Position::new(300.0, 400.0),
components::OrbitBody::new(0.0, 10.0),
components::Gravity,
));
node_world.push((components::Edge::new(n0, n1),));
node_world.push((components::Edge::new(n1, n2),));
node_world.push((components::Edge::new(n0, n2),));
node_world.push((components::Edge::new(n2, n3),));
node_world.push((components::Edge::new(n3, n4),));
node_world.push((components::Edge::new(n0, n4),));
node_world.push((components::TheSun, components::Position::new(600.0, 540.0)));
let node_schedule = legion::Schedule::builder()
.add_system(components::update_gravity_system())
.add_system(components::update_movement_system())
.add_thread_local(components::update_nodes_system())
.flush()
.add_thread_local(components::draw_edges_system())
.add_thread_local(components::draw_nodes_system())
.add_thread_local(components::draw_thesun_system())
.build();
(node_world, resources, node_schedule)
}
use crate::angel_shifts;
use crate::colliders;
use crate::colors;
use crate::components;
use crate::entities;
use crate::gamestate;
use crate::resources;
use crate::states;
use crate::svg_loader;
use crate::systems;
use crate::utils;
......@@ -17,7 +19,12 @@ pub struct InGameState {
}
impl InGameState {
pub fn new(level: svg_loader::SvgLevel, mut rendering: resources::Rendering) -> InGameState {
pub fn new(
level: svg_loader::SvgLevel,
mut rendering: resources::Rendering,
player: components::Player,
assigned_shift: angel_shifts::AngelShift,
) -> InGameState {
let sanity_bar = utils::get_element_by_id("sanity-amount").unwrap();
let foreground = rendering.register_image(level.foreground_image.clone());
......@@ -31,32 +38,35 @@ impl InGameState {
resources.insert(resources::Camera::new(1920.0, 1080.0));
resources.insert(colliders::CollisionWorld::new());
let player = entities::create_player(&mut world, &resources, &level);
let player = entities::create_player(&mut world, &resources, &level, player);
resources.insert(resources::Player(player));
entities::create_drop_points(&mut world, &level);
resources.insert(resources::GameManager::new());
level.apply_colliders(&mut world);
let schedule = legion::Schedule::builder()
let mut schedule_builder = legion::Schedule::builder();
schedule_builder
.add_system(colliders::synchronize_collisision_world_system())
.flush()
.add_system(colliders::update_collision_world_system())
.flush()
.add_thread_local(systems::move_movable_system())
.add_thread_local(systems::sanity_goes_up_and_down_system())
.add_thread_local(systems::collect_bottledrops_system())
.flush();
assigned_shift.init_gameworld(&mut world, &mut resources, &mut schedule_builder, &level);
schedule_builder
.flush()
.add_thread_local(systems::move_camera_to_player_system())
.flush()
.add_thread_local(systems::camera_system())
.add_thread_local(systems::update_sanity_bar_system(sanity_bar))
.flush()
.add_thread_local(systems::draw_level_layer_system(background))
.add_thread_local(systems::draw_sprites_system())
.add_thread_local(systems::draw_level_layer_system(foreground))
.add_thread_local(systems::draw_tmp_matebottledrop_system())
// .add_thread_local(systems::draw_debug_colliders_system())
.build();
;
let schedule = schedule_builder.build();
InGameState {
gui_svg: utils::get_element_by_id("ingame-ui").unwrap(),
......@@ -114,6 +124,8 @@ impl gamestate::State for InGameState {
}
fn update(&mut self) -> gamestate::Transition {
use legion::IntoQuery;
{
let rendering = self.resources.get::<resources::Rendering>().unwrap();
rendering.set_transform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
......@@ -121,6 +133,20 @@ impl gamestate::State for InGameState {
rendering.fill_rect(0.0, 0.0, 1920.0, 1080.0);
}
self.schedule.execute(&mut self.world, &mut self.resources);
if self
.resources
.get::<resources::GameManager>()
.unwrap()
.wants_return_to_heaven()
{
let player_ent = self.resources.get::<resources::Player>().unwrap().0.clone();
let player = <&components::Player>::query()
.get(&self.world, player_ent)
.unwrap();
gamestate::Transition::replace(states::HeavenState::new(Some(player.clone())))
} else {
gamestate::Transition::Loop
}
}
}
use crate::angel_shifts;
use crate::colors;
use crate::components;
use crate::gamestate;
......@@ -10,10 +11,16 @@ pub struct LevelLoadingState {
node_world: legion::World,
resources: legion::Resources,
node_schedule: legion::Schedule,
player: components::Player,
assigned_shift: Option<angel_shifts::AngelShift>,
}
impl LevelLoadingState {
pub fn new() -> LevelLoadingState {
pub fn new(
player: components::Player,
assigned_shift: angel_shifts::AngelShift,
) -> LevelLoadingState {
let mut resources = legion::Resources::default();
resources.insert(resources::Clock::new());
resources.insert(resources::Rendering::new("game-canvas").unwrap());
......@@ -32,6 +39,8 @@ impl LevelLoadingState {
node_world,
resources,
node_schedule,
player,
assigned_shift: Some(assigned_shift),
}
}
}
......@@ -40,8 +49,14 @@ impl gamestate::State for LevelLoadingState {
fn init(&mut self, init: gamestate::StateInitializer) -> gamestate::Transition {
let mut rendering = resources::Rendering::new("game-canvas").unwrap();
let handle = init.get_handle();
let player = self.player.clone();
let assigned_shift = self.assigned_shift.take().unwrap();
wasm_bindgen_futures::spawn_local(async move {
let level = svg_loader::SvgLevel::load_from_svg_file("resources/testlevel.svg")
let level_name = assigned_shift.level_name();
let level = svg_loader::SvgLevel::load_from_svg_file(&format!(
"resources/levels/{}",
level_name
))
.await
.unwrap();
......@@ -50,7 +65,10 @@ impl gamestate::State for LevelLoadingState {
.await;
handle.do_transition(gamestate::Transition::replace(states::InGameState::new(
level, rendering,
level,
rendering,
player.clone(),
assigned_shift,
)));
});
gamestate::Transition::Loop
......
......@@ -25,7 +25,7 @@ impl gamestate::State for MainMenuState {
// In debug builds, skip the main menu and go straight to heaven. This eases iterative
// development because one does not need to click "Play Game" every time ...