aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/maps/campaign/lobby.yaml10
-rw-r--r--server/src/entity/campaign.rs87
-rw-r--r--server/src/entity/customers.rs1
-rw-r--r--server/src/entity/mod.rs33
-rw-r--r--server/src/scoreboard.rs11
-rw-r--r--server/src/server.rs50
6 files changed, 179 insertions, 13 deletions
diff --git a/data/maps/campaign/lobby.yaml b/data/maps/campaign/lobby.yaml
index 215832b5..fbaa96ed 100644
--- a/data/maps/campaign/lobby.yaml
+++ b/data/maps/campaign/lobby.yaml
@@ -18,10 +18,10 @@ map:
- "'*''''''''''''*'"
- "*'''''*'''*'''''"
- "'*''*''''*'*'*'*"
- - "*''''''''''''*''"
- - "*'''''''...''**'"
- - "''''''''.1.'''''"
- - "''*'''''...''*'*"
+ - "*'''''''█████*''"
+ - "*'''''''█.1.█**'"
+ - "''''''''█...█'''"
+ - "''*'''''██a██*'*"
- "'*'''''~''''''''"
- "''''''''''''''*'"
- "'*'''''''''''*''"
@@ -40,6 +40,7 @@ tiles:
"~": floor
".": floor
"1": floor
+ "a": floor
"b": book
"'": grass
"*": tree
@@ -50,6 +51,7 @@ customer_spawn: "!"
tile_entities:
"1": !map { name: "campaign/01" }
+ "a": !gate { condition: !all [!stars [sophomore, 2], !stars [paris, 1]] }
walkable:
- floor
diff --git a/server/src/entity/campaign.rs b/server/src/entity/campaign.rs
index c4c8ab52..934f7542 100644
--- a/server/src/entity/campaign.rs
+++ b/server/src/entity/campaign.rs
@@ -16,8 +16,14 @@
*/
use super::{Entity, EntityContext};
+use crate::{scoreboard::ScoreboardStore, server::GameServerExt};
use anyhow::Result;
-use hurrycurry_protocol::glam::Vec2;
+use hurrycurry_protocol::{
+ glam::{IVec2, Vec2},
+ PacketC, PlayerID, TileIndex,
+};
+use serde::{Deserialize, Serialize};
+use std::fmt::Write;
#[derive(Debug, Default, Clone)]
pub struct Map {
@@ -39,3 +45,82 @@ impl Entity for Map {
Ok(())
}
}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum GateCondition {
+ All(Vec<GateCondition>),
+ Any(Vec<GateCondition>),
+ Stars(String, u8),
+}
+
+#[derive(Debug, Clone)]
+pub struct Gate {
+ pub active: bool,
+ pub unlocked: bool,
+ pub location: IVec2,
+ pub blocker_tile: TileIndex,
+ pub condition: GateCondition,
+}
+impl Entity for Gate {
+ fn tick(&mut self, c: EntityContext<'_>) -> Result<()> {
+ if self.active {
+ self.active = false;
+ self.unlocked = self.condition.check(c.scoreboard);
+ if !self.unlocked {
+ c.game
+ .set_tile(self.location, Some(self.blocker_tile), c.packet_out)
+ }
+ }
+ Ok(())
+ }
+ fn interact(
+ &mut self,
+ c: EntityContext<'_>,
+ pos: Option<IVec2>,
+ _player: PlayerID,
+ ) -> Result<bool> {
+ if !self.unlocked && pos == Some(self.location) {
+ c.packet_out.push_back(PacketC::ServerMessage {
+ text: format!("To unlock: \n\n{}", self.condition.show(c.scoreboard)),
+ });
+ return Ok(true);
+ }
+ Ok(false)
+ }
+}
+
+impl GateCondition {
+ fn check(&self, scoreboard: &ScoreboardStore) -> bool {
+ match self {
+ GateCondition::All(cs) => cs.iter().all(|c| c.check(scoreboard)),
+ GateCondition::Any(cs) => cs.iter().any(|c| c.check(scoreboard)),
+ GateCondition::Stars(map, thres) => scoreboard.get(map).map_or(false, |s| {
+ s.best.first().map_or(false, |b| b.score.stars >= *thres)
+ }),
+ }
+ }
+ pub fn show(&self, scoreboard: &ScoreboardStore) -> String {
+ match self {
+ GateCondition::All(cs) => {
+ let mut o = String::new();
+ for c in cs {
+ if !c.check(scoreboard) {
+ writeln!(o, "- {}", c.show(scoreboard)).unwrap();
+ }
+ }
+ o
+ }
+ GateCondition::Any(cs) => {
+ let mut o = String::new();
+ for c in cs {
+ writeln!(o, "- {}", c.show(scoreboard)).unwrap();
+ }
+ o
+ }
+ GateCondition::Stars(map, thres) => {
+ format!("Reach at least {thres} stars in {map}")
+ }
+ }
+ }
+}
diff --git a/server/src/entity/customers.rs b/server/src/entity/customers.rs
index d75f5b51..da905bc0 100644
--- a/server/src/entity/customers.rs
+++ b/server/src/entity/customers.rs
@@ -54,6 +54,7 @@ impl Entity for Customers {
packet_in: c.packet_in,
score_changed: c.score_changed,
dt: c.dt,
+ scoreboard: c.scoreboard,
load_map: c.load_map,
})?;
}
diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs
index 8055b50d..c8424934 100644
--- a/server/src/entity/mod.rs
+++ b/server/src/entity/mod.rs
@@ -23,16 +23,16 @@ pub mod environment_effect;
pub mod item_portal;
pub mod player_portal;
-use crate::data::ItemTileRegistry;
+use crate::{data::ItemTileRegistry, scoreboard::ScoreboardStore};
use anyhow::{anyhow, Result};
-use campaign::Map;
+use campaign::{Gate, GateCondition, Map};
use conveyor::Conveyor;
use customers::Customers;
use environment_effect::{EnvironmentController, EnvironmentEffect, EnvironmentEffectController};
use hurrycurry_client_lib::Game;
use hurrycurry_protocol::{
glam::{IVec2, Vec2},
- PacketC, PacketS,
+ PacketC, PacketS, PlayerID,
};
use item_portal::ItemPortal;
use player_portal::PlayerPortal;
@@ -48,12 +48,21 @@ pub struct EntityContext<'a> {
pub packet_in: &'a mut VecDeque<PacketS>,
pub score_changed: &'a mut bool,
pub load_map: &'a mut Option<String>,
+ pub scoreboard: &'a ScoreboardStore,
pub dt: f32,
}
pub trait Entity {
fn tick(&mut self, c: EntityContext<'_>) -> Result<()>;
fn destructor(&mut self, _c: EntityContext<'_>) {}
+ fn interact(
+ &mut self,
+ _c: EntityContext<'_>,
+ _pos: Option<IVec2>,
+ _player: PlayerID,
+ ) -> Result<bool> {
+ Ok(false)
+ }
}
// macro_rules! entities {
@@ -104,6 +113,10 @@ pub enum EntityDecl {
},
EnvironmentEffect(EnvironmentEffect),
Environment(Vec<String>),
+ Gate {
+ location: Option<IVec2>,
+ condition: GateCondition,
+ },
}
pub fn construct_entity(
@@ -113,7 +126,9 @@ pub fn construct_entity(
) -> Result<DynEntity> {
Ok(match decl.to_owned() {
EntityDecl::ItemPortal { from, to } => Box::new(ItemPortal {
- from: from.or(pos).ok_or(anyhow!("Item portal start without start"))?,
+ from: from
+ .or(pos)
+ .ok_or(anyhow!("Item portal start without start"))?,
to,
}),
EntityDecl::PlayerPortal { from, to } => Box::new(PlayerPortal {
@@ -149,6 +164,16 @@ pub fn construct_entity(
.ok_or(anyhow!("no location"))?,
name,
}),
+ EntityDecl::Gate {
+ condition,
+ location,
+ } => Box::new(Gate {
+ condition,
+ unlocked: false,
+ location: location.or(pos).ok_or(anyhow!("no location"))?,
+ blocker_tile: reg.register_tile("fence".to_string()),
+ active: true,
+ }),
EntityDecl::Customers {} => Box::new(Customers::new()?),
EntityDecl::EnvironmentEffect(config) => Box::new(EnvironmentEffectController::new(config)),
EntityDecl::Environment(names) => Box::new(EnvironmentController(names)),
diff --git a/server/src/scoreboard.rs b/server/src/scoreboard.rs
index e546f973..f61b3001 100644
--- a/server/src/scoreboard.rs
+++ b/server/src/scoreboard.rs
@@ -30,13 +30,13 @@ pub struct ScoreboardStore {
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Scoreboard {
- plays: usize,
- best: Vec<ScoreboardEntry>,
+ pub plays: usize,
+ pub best: Vec<ScoreboardEntry>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ScoreboardEntry {
- players: Vec<String>,
- score: Score,
+ pub players: Vec<String>,
+ pub score: Score,
}
impl ScoreboardStore {
@@ -62,6 +62,9 @@ impl ScoreboardStore {
rename(buffer_path, path).await?;
Ok(())
}
+ pub fn get<'a>(&'a self, map: &str) -> Option<&'a Scoreboard> {
+ self.maps.get(map)
+ }
pub fn insert(&mut self, map: &str, players: Vec<String>, score: Score) {
let b = self.maps.entry(map.to_owned()).or_default();
b.plays += 1;
diff --git a/server/src/server.rs b/server/src/server.rs
index bee94b0e..63664354 100644
--- a/server/src/server.rs
+++ b/server/src/server.rs
@@ -72,6 +72,7 @@ pub trait GameServerExt {
packet_out: Option<&mut VecDeque<PacketC>>,
);
fn prime_client(&self) -> Vec<PacketC>;
+ fn set_tile(&mut self, p: IVec2, t: Option<TileIndex>, packet_out: &mut VecDeque<PacketC>);
}
impl GameServerExt for Game {
fn unload(&mut self, packet_out: &mut VecDeque<PacketC>) {
@@ -229,6 +230,35 @@ impl GameServerExt for Game {
});
}
}
+
+ fn set_tile(
+ &mut self,
+ tile: IVec2,
+ kind: Option<TileIndex>,
+ packet_out: &mut VecDeque<PacketC>,
+ ) {
+ if let Some(kind) = kind {
+ self.tiles.insert(tile, Tile::from(kind));
+ if self.data.is_tile_colliding(kind) {
+ self.walkable.remove(&tile);
+ } else {
+ self.walkable.insert(tile);
+ }
+ } else {
+ self.tiles.remove(&tile);
+ self.walkable.remove(&tile);
+ }
+ packet_out.push_back(PacketC::UpdateMap {
+ tile,
+ kind,
+ neighbors: [
+ self.tiles.get(&(tile + IVec2::NEG_Y)).map(|e| e.kind),
+ self.tiles.get(&(tile + IVec2::NEG_X)).map(|e| e.kind),
+ self.tiles.get(&(tile + IVec2::Y)).map(|e| e.kind),
+ self.tiles.get(&(tile + IVec2::X)).map(|e| e.kind),
+ ],
+ })
+ }
}
impl Server {
@@ -267,6 +297,7 @@ impl Server {
packet_out: &mut self.packet_out,
packet_in: &mut self.packet_loopback,
score_changed: &mut self.score_changed,
+ scoreboard: &self.scoreboard,
dt: 0.,
load_map: &mut None,
});
@@ -345,6 +376,24 @@ impl Server {
}
}
PacketS::Interact { pos, player } => {
+ for e in &mut self.entities {
+ if e.interact(
+ EntityContext {
+ game: &mut self.game,
+ packet_out: &mut self.packet_out,
+ packet_in: &mut self.packet_loopback,
+ score_changed: &mut self.score_changed,
+ load_map: &mut None,
+ scoreboard: &self.scoreboard,
+ dt: 0.,
+ },
+ pos,
+ player,
+ )? {
+ return Ok(());
+ }
+ }
+
let pid = player;
let player = self
.game
@@ -620,6 +669,7 @@ impl Server {
packet_out: &mut self.packet_out,
score_changed: &mut self.score_changed,
packet_in: &mut self.packet_loopback,
+ scoreboard: &self.scoreboard,
dt,
}) {
warn!("Entity tick failed: {e}")