1
0
mirror of https://github.com/osmarks/ewo3.git synced 2025-01-02 21:40:36 +00:00

useful items, multitile positions, etc

This commit is contained in:
osmarks 2024-06-18 19:31:24 +01:00
parent db02d05778
commit fbf154fa7c
5 changed files with 397 additions and 121 deletions

17
Cargo.lock generated
View File

@ -130,6 +130,12 @@ dependencies = [
"crypto-common",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "euclid"
version = "0.22.10"
@ -149,6 +155,7 @@ dependencies = [
"fastrand",
"futures-util",
"hecs",
"indexmap",
"lazy_static",
"noise-functions",
"seahash",
@ -286,6 +293,16 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "indexmap"
version = "2.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.11"

View File

@ -19,4 +19,5 @@ serde_json = "1"
slab = "0.4"
lazy_static = "1"
seahash = "4"
noise-functions = "0.2"
noise-functions = "0.2"
indexmap = "2"

View File

@ -78,6 +78,11 @@
{#if health}
Your health is {health}.
{/if}
<ul>
{#each inventory as item}
<li>{item[0]} x{item[2]}: {item[1]}</li>
{/each}
</ul>
</div>
</div>
@ -89,6 +94,7 @@
let dead = false
let health
let players
let inventory = []
let ws
const connect = () => {
@ -105,6 +111,7 @@
}
grid = newGrid
health = data.Display.health
inventory = data.Display.inventory
}
if (data === "Dead") {
dead = true
@ -164,7 +171,8 @@
"a": "Left",
"d": "Right",
"z": "DownLeft",
"x": "DownRight"
"x": "DownRight",
"f": "Dig"
}
connect()

View File

@ -1,12 +1,13 @@
use hecs::{Entity, World};
use hecs::{CommandBuffer, Entity, World};
use euclid::{Point3D, Point2D, Vector2D};
use futures_util::{stream::TryStreamExt, SinkExt, StreamExt};
use indexmap::IndexMap;
use noise_functions::Sample3;
use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::tungstenite::protocol::Message;
use tokio::sync::{mpsc, Mutex};
use anyhow::{Result, Context, anyhow};
use std::{collections::{hash_map::Entry, HashMap}, hash::{Hash, Hasher}, net::SocketAddr, sync::Arc, thread::current, time::Duration};
use std::{collections::{hash_map::Entry, HashMap, HashSet, VecDeque}, convert::TryFrom, hash::{Hash, Hasher}, net::SocketAddr, sync::Arc, time::Duration};
use slab::Slab;
use serde::{Serialize, Deserialize};
@ -90,8 +91,9 @@ enum Input {
#[derive(Serialize, Deserialize, Clone)]
enum Frame {
Dead,
Display { nearby: Vec<(i64, i64, char, f32)>, health: f32 },
PlayerCount(usize)
Display { nearby: Vec<(i64, i64, char, f32)>, health: f32, inventory: Vec<(String, String, u64)> },
PlayerCount(usize),
Message(String)
}
struct Client {
@ -106,16 +108,45 @@ struct GameState {
ticks: u64
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Item {
Dirt,
Bone
}
impl Item {
fn name(&self) -> &'static str {
use Item::*;
match self {
Dirt => "Dirt",
Bone => "Bone"
}
}
fn description(&self) -> &'static str {
use Item::*;
match self {
Dirt => "It's from the ground. You're carrying it for some reason.",
Bone => "Disassembling your enemies for resources is probably ethical."
}
}
}
#[derive(Debug, Clone)]
struct PlayerCharacter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Position(Coord);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Position(VecDeque<Coord>);
impl Position {
fn head(&self) -> Coord {
*self.0.front().unwrap()
}
fn single_tile(c: Coord) -> Self {
Self(VecDeque::from([c]))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct MovingInto(Coord);
@ -123,6 +154,13 @@ struct MovingInto(Coord);
#[derive(Debug, Clone)]
struct Health(f32, f32);
impl Health {
fn pct(&self) -> f32 {
if self.1 == 0.0 { 0.0 }
else { self.0 / self.1 }
}
}
#[derive(Debug, Clone)]
struct Render(char);
@ -153,20 +191,17 @@ struct Collidable;
#[derive(Debug, Clone)]
struct Velocity(CoordVec);
#[derive(Debug, Clone)]
struct DeferredRandomly<T: Clone + std::fmt::Debug + hecs::Bundle>(u64, T);
#[derive(Debug, Clone)]
struct Terrain;
#[derive(Debug, Clone)]
struct Obstruction { entry_cost: StochasticNumber, exit_cost: StochasticNumber }
struct Obstruction { entry_multiplier: f32, exit_multiplier: f32 }
#[derive(Debug, Clone)]
struct Energy { current: f32, regeneration_rate: f32, burst: f32 }
#[derive(Debug, Clone)]
struct DespawnOnImpact;
struct Drops(Vec<(Item, StochasticNumber)>);
impl Energy {
fn try_consume(&mut self, cost: f32) -> bool {
@ -179,6 +214,46 @@ impl Energy {
}
}
#[derive(Debug, Clone)]
struct DespawnOnImpact;
#[derive(Debug, Clone)]
struct Inventory(indexmap::IndexMap<Item, u64>);
impl Inventory {
fn add(&mut self, item: Item, qty: u64) {
*self.0.entry(item).or_default() += qty;
}
fn take(&mut self, item: Item, qty: u64) -> bool {
match self.0.entry(item) {
indexmap::map::Entry::Occupied(mut o) => {
let current = o.get_mut();
if *current >= qty {
*current -= qty;
return true;
}
return false;
},
indexmap::map::Entry::Vacant(_) => return false
}
}
fn extend(&mut self, other: &Inventory) {
for (item, count) in other.0.iter() {
self.add(item.clone(), *count);
}
}
fn is_empty(&self) -> bool {
self.0.iter().any(|(_, c)| *c > 0)
}
fn empty() -> Self {
Self(IndexMap::new())
}
}
const VIEW: i64 = 15;
const WALL: i64 = 128;
const RANDOM_DESPAWN_INV_RATE: u64 = 4000;
@ -239,21 +314,22 @@ struct EnemySpec {
initial_health: f32,
move_delay: usize,
attack_cooldown: u64,
ranged: bool
ranged: bool,
drops: Vec<(Item, StochasticNumber)>
}
impl EnemySpec {
// Numbers ported from original EWO. Fudge constants added elsewhere.
fn random() -> EnemySpec {
match fastrand::usize(0..650) {
0..=99 => EnemySpec { symbol: 'I', min_damage: 10.0, damage_range: 5.0, initial_health: 50.0, move_delay: 70, attack_cooldown: 10, ranged: false }, // IBIS
100..=199 => EnemySpec { symbol: 'K', min_damage: 5.0, damage_range: 15.0, initial_health: 30.0, move_delay: 40, attack_cooldown: 10, ranged: false }, // KESTREL
200..=299 => EnemySpec { symbol: 'S', min_damage: 5.0, damage_range: 5.0, initial_health: 20.0, move_delay: 50, attack_cooldown: 10, ranged: false }, // SNAKE
300..=399 => EnemySpec { symbol: 'E', min_damage: 10.0, damage_range: 20.0, initial_health: 80.0, move_delay: 80, attack_cooldown: 10, ranged: false }, // EMU
400..=499 => EnemySpec { symbol: 'O', min_damage: 8.0, damage_range: 17.0, initial_health: 150.0, move_delay: 100, attack_cooldown: 10, ranged: false }, // OGRE
500..=599 => EnemySpec { symbol: 'R', min_damage: 5.0, damage_range: 5.0, initial_health: 15.0, move_delay: 40, attack_cooldown: 10, ranged: false }, // RAT
600..=609 => EnemySpec { symbol: 'M' , min_damage: 20.0, damage_range: 10.0, initial_health: 150.0, move_delay: 70, attack_cooldown: 10, ranged: false }, // MOA
610..=649 => EnemySpec { symbol: 'P', min_damage: 10.0, damage_range: 5.0, initial_health: 15.0, move_delay: 20, attack_cooldown: 10, ranged: true }, // PLATYPUS
0..=99 => EnemySpec { symbol: 'I', min_damage: 10.0, damage_range: 5.0, initial_health: 50.0, move_delay: 70, attack_cooldown: 10, ranged: false, drops: vec![] }, // IBIS
100..=199 => EnemySpec { symbol: 'K', min_damage: 5.0, damage_range: 15.0, initial_health: 30.0, move_delay: 40, attack_cooldown: 10, ranged: false, drops: vec![] }, // KESTREL
200..=299 => EnemySpec { symbol: 'S', min_damage: 5.0, damage_range: 5.0, initial_health: 20.0, move_delay: 50, attack_cooldown: 10, ranged: false, drops: vec![] }, // SNAKE
300..=399 => EnemySpec { symbol: 'E', min_damage: 10.0, damage_range: 20.0, initial_health: 80.0, move_delay: 80, attack_cooldown: 10, ranged: false, drops: vec![] }, // EMU
400..=499 => EnemySpec { symbol: 'O', min_damage: 8.0, damage_range: 17.0, initial_health: 150.0, move_delay: 100, attack_cooldown: 10, ranged: false, drops: vec![] }, // OGRE
500..=599 => EnemySpec { symbol: 'R', min_damage: 5.0, damage_range: 5.0, initial_health: 15.0, move_delay: 40, attack_cooldown: 10, ranged: false, drops: vec![] }, // RAT
600..=609 => EnemySpec { symbol: 'M' , min_damage: 20.0, damage_range: 10.0, initial_health: 150.0, move_delay: 70, attack_cooldown: 10, ranged: false, drops: vec![] }, // MOA
610..=649 => EnemySpec { symbol: 'P', min_damage: 10.0, damage_range: 5.0, initial_health: 15.0, move_delay: 20, attack_cooldown: 10, ranged: true, drops: vec![] }, // PLATYPUS
_ => unreachable!()
}
}
@ -308,6 +384,10 @@ impl StochasticNumber {
}
}
fn sample_rounded<T: TryFrom<i128>>(&self) -> T {
T::try_from(self.sample().round() as i128).map_err(|_| "convert fail").unwrap()
}
fn triangle_from_min_range(min: f32, range: f32) -> Self {
StochasticNumber::Triangle { min: min, max: min + range, mode: (min + range) / 2.0 }
}
@ -318,41 +398,48 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
let mut positions = HashMap::new();
for (entity, pos) in state.world.query_mut::<hecs::With<&Position, &Collidable>>() {
positions.insert(pos.0, entity);
for subpos in pos.0.iter() {
positions.insert(*subpos, entity);
}
}
for (entity, pos) in state.world.query_mut::<hecs::With<&Position, &Terrain>>() {
terrain_positions.insert(pos.0, entity);
for subpos in pos.0.iter() {
terrain_positions.insert(*subpos, entity);
}
}
let mut buffer = hecs::CommandBuffer::new();
// Spawn enemies
for (_entity, (Position(pos), EnemyTarget { spawn_range, spawn_density, spawn_rate_inv, .. })) in state.world.query::<(&Position, &EnemyTarget)>().iter() {
for (_entity, (pos, EnemyTarget { spawn_range, spawn_density, spawn_rate_inv, .. })) in state.world.query::<(&Position, &EnemyTarget)>().iter() {
let pos = pos.head();
if fastrand::usize(0..*spawn_rate_inv) == 0 {
let c = count_hexes(*spawn_range.end());
let mut newpos = *pos + sample_range(*spawn_range.end());
let mut newpos = pos + sample_range(*spawn_range.end());
let mut occupied = false;
for _ in 0..(c as f32 / spawn_density * 0.005).ceil() as usize {
if positions.contains_key(&newpos) {
occupied = true;
break;
}
newpos = *pos + sample_range(*spawn_range.end());
newpos = pos + sample_range(*spawn_range.end());
}
if !occupied && get_base_terrain(newpos).can_enter() && hex_distance(newpos, *pos) >= *spawn_range.start() {
let spec = EnemySpec::random();
if !occupied && get_base_terrain(newpos).can_enter() && hex_distance(newpos, pos) >= *spawn_range.start() {
let mut spec = EnemySpec::random();
spec.drops.push((Item::Bone, StochasticNumber::Triangle { min: 0.7 * spec.initial_health / 40.0, max: 1.3 * spec.initial_health / 40.0, mode: spec.initial_health / 40.0 }));
if spec.ranged {
buffer.spawn((
Render(spec.symbol),
Health(spec.initial_health, spec.initial_health),
Enemy,
RangedAttack { damage: StochasticNumber::triangle_from_min_range(spec.min_damage, spec.damage_range), energy: spec.attack_cooldown as f32, range: 4 },
Position(newpos),
Position::single_tile(newpos),
MoveCost(StochasticNumber::Triangle { min: 0.0, max: 2.0 * spec.move_delay as f32 / 3.0, mode: spec.move_delay as f32 / 3.0 }),
Collidable,
DespawnRandomly(RANDOM_DESPAWN_INV_RATE),
Energy { regeneration_rate: 1.0, current: 0.0, burst: 0.0 }
Energy { regeneration_rate: 1.0, current: 0.0, burst: 0.0 },
Drops(spec.drops)
));
} else {
buffer.spawn((
@ -360,11 +447,12 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Health(spec.initial_health, spec.initial_health),
Enemy,
Attack { damage: StochasticNumber::triangle_from_min_range(spec.min_damage, spec.damage_range), energy: spec.attack_cooldown as f32 },
Position(newpos),
Position::single_tile(newpos),
MoveCost(StochasticNumber::Triangle { min: 0.0, max: 2.0 * spec.move_delay as f32 / 3.0, mode: spec.move_delay as f32 / 3.0 }),
Collidable,
DespawnRandomly(RANDOM_DESPAWN_INV_RATE),
Energy { regeneration_rate: 1.0, current: 0.0, burst: 0.0 }
Energy { regeneration_rate: 1.0, current: 0.0, burst: 0.0 },
Drops(spec.drops)
));
}
}
@ -372,11 +460,13 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
}
// Process enemy motion and ranged attacks
for (entity, (Position(pos), ranged, energy)) in state.world.query::<hecs::With<(&Position, Option<&mut RangedAttack>, Option<&mut Energy>), &Enemy>>().iter() {
for (entity, (pos, ranged, energy)) in state.world.query::<hecs::With<(&Position, Option<&mut RangedAttack>, Option<&mut Energy>), &Enemy>>().iter() {
let pos = pos.head();
for direction in DIRECTIONS.iter() {
if let Some(target) = positions.get(&(*pos + *direction)) {
if let Some(target) = positions.get(&(pos + *direction)) {
if let Ok(_) = state.world.get::<&EnemyTarget>(*target) {
buffer.insert_one(entity, MovingInto(*pos + *direction));
buffer.insert_one(entity, MovingInto(pos + *direction));
continue;
}
}
@ -386,11 +476,12 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
// TODO we maybe need a spatial index for this
for (_entity, (target_pos, target)) in state.world.query::<(&Position, &EnemyTarget)>().iter() {
let distance = hex_distance(*pos, target_pos.0);
let target_pos = target_pos.head();
let distance = hex_distance(pos, target_pos);
if distance < target.aggression_range {
match closest {
Some((_pos, old_distance)) if old_distance < distance => closest = Some((target_pos.0, distance)),
None => closest = Some((target_pos.0, distance)),
Some((_pos, old_distance)) if old_distance < distance => closest = Some((target_pos, distance)),
None => closest = Some((target_pos, distance)),
_ => ()
}
}
@ -400,10 +491,10 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
if let Some(ranged_attack) = ranged {
// slightly smart behaviour for ranged attacker: try to stay just within range
let direction = DIRECTIONS.iter().min_by_key(|dir|
(hex_distance(*pos + **dir, target_pos) - (ranged_attack.range as i64 - 1)).abs()).unwrap();
buffer.insert_one(entity, MovingInto(*pos + *direction));
(hex_distance(pos + **dir, target_pos) - (ranged_attack.range as i64 - 1)).abs()).unwrap();
buffer.insert_one(entity, MovingInto(pos + *direction));
// do ranged attack if valid
let atk_dir = target_pos - *pos;
let atk_dir = target_pos - pos;
if on_axis(atk_dir) && (energy.is_none() || energy.unwrap().try_consume(ranged_attack.energy)) {
let atk_dir = atk_dir.clamp(-CoordVec::one(), CoordVec::one());
buffer.spawn((
@ -411,23 +502,23 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Enemy,
Attack { damage: ranged_attack.damage, energy: 0.0 },
Velocity(atk_dir),
Position(*pos),
Position::single_tile(pos),
DespawnOnTick(state.ticks.wrapping_add(ranged_attack.range))
));
}
} else {
let direction = DIRECTIONS.iter().min_by_key(|dir| hex_distance(*pos + **dir, target_pos)).unwrap();
buffer.insert_one(entity, MovingInto(*pos + *direction));
let direction = DIRECTIONS.iter().min_by_key(|dir| hex_distance(pos + **dir, target_pos)).unwrap();
buffer.insert_one(entity, MovingInto(pos + *direction));
}
} else {
// wander randomly (ethical)
buffer.insert_one(entity, MovingInto(*pos + *fastrand::choice(DIRECTIONS).unwrap()));
buffer.insert_one(entity, MovingInto(pos + *fastrand::choice(DIRECTIONS).unwrap()));
}
}
// Process velocity
for (entity, (Position(pos), Velocity(vel))) in state.world.query_mut::<(&Position, &Velocity)>() {
buffer.insert_one(entity, MovingInto(*pos + *vel));
for (entity, (pos, Velocity(vel))) in state.world.query_mut::<(&Position, &Velocity)>() {
buffer.insert_one(entity, MovingInto(pos.head() + *vel));
}
buffer.run_on(&mut state.world);
@ -435,6 +526,9 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
// Process inputs
for (_id, client) in state.clients.iter_mut() {
let mut next_movement = CoordVec::zero();
let position = state.world.get::<&Position>(client.entity)?.head();
let mut energy = state.world.get::<&mut Energy>(client.entity)?;
let mut inventory = state.world.get::<&mut Inventory>(client.entity)?;
loop {
let recv = client.inputs_rx.try_recv();
match recv {
@ -447,28 +541,81 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Input::DownRight => next_movement = CoordVec::new(0, 1),
Input::DownLeft => next_movement = CoordVec::new(-1, 1),
Input::Dig => {
if terrain_positions.get(&position).is_none() && energy.try_consume(5.0) {
buffer.spawn((
Terrain,
Render('_'),
Obstruction { entry_multiplier: 5.0, exit_multiplier: 5.0 },
DespawnOnTick(state.ticks.wrapping_add(StochasticNumber::triangle_from_min_range(5000.0, 5000.0).sample().round() as u64)),
Position::single_tile(position)
));
inventory.add(Item::Dirt, StochasticNumber::triangle_from_min_range(1.0, 3.0).sample_rounded());
}
}
},
Err(e) => return Err(e.into())
}
}
let position = state.world.get::<&mut Position>(client.entity)?.0;
let target = position + next_movement;
if get_base_terrain(target).can_enter() && target != position {
state.world.insert_one(client.entity, MovingInto(target)).unwrap();
buffer.insert_one(client.entity, MovingInto(target));
}
}
// Process motion and attacks
for (entity, (Position(current_pos), MovingInto(target_pos), damage, mut energy, move_cost, despawn_on_impact)) in state.world.query::<(&mut Position, &MovingInto, Option<&mut Attack>, Option<&mut Energy>, Option<&MoveCost>, Option<&DespawnOnImpact>)>().iter() {
let mut move_cost = move_cost.map(|x| x.0.sample()).unwrap_or(0.0);
if let Some(current_terrain) = terrain_positions.get(current_pos) {
move_cost += 1.0;
buffer.run_on(&mut state.world);
let mut despawn_buffer = HashSet::new();
// This might lead to a duping glitch, which would at least be funny.
let kill = |buffer: &mut CommandBuffer, despawn_buffer: &mut HashSet<Entity>, state: &GameState, entity: Entity, killer: Option<Entity>, position: Option<Coord>| {
let position = position.unwrap_or_else(|| state.world.get::<&Position>(entity).unwrap().head());
despawn_buffer.insert(entity);
buffer.despawn(entity);
let mut materialized_drops = Inventory::empty();
if let Ok(drops) = state.world.get::<&Drops>(entity) {
for (drop, frequency) in drops.0.iter() {
materialized_drops.add(drop.clone(), frequency.sample_rounded())
}
}
// TODO will break attacks kind of, desirable? Doubtful.
if let Ok(other_inv) = state.world.get::<&Inventory>(entity) {
materialized_drops.extend(&other_inv);
}
let killer_consumed_items = if let Some(killer) = killer {
if let Ok(mut inv) = state.world.get::<&mut Inventory>(killer) {
inv.extend(&materialized_drops);
true
} else {
false
}
} else { false };
if !killer_consumed_items && !materialized_drops.is_empty() {
buffer.spawn((
Position::single_tile(position),
Render('☒'),
materialized_drops
));
}
};
// Process motion and attacks
for (entity, (current_pos, MovingInto(target_pos), damage, mut energy, move_cost, despawn_on_impact)) in state.world.query::<(&mut Position, &MovingInto, Option<&mut Attack>, Option<&mut Energy>, Option<&MoveCost>, Option<&DespawnOnImpact>)>().iter() {
let mut move_cost = move_cost.map(|x| x.0.sample()).unwrap_or(0.0);
for tile in current_pos.0.iter() {
// TODO: perhaps large enemies should not be exponentially more vulnerable to environmental hazards
if let Some(current_terrain) = terrain_positions.get(tile) {
if let Ok(obstruction) = state.world.get::<&Obstruction>(*current_terrain) {
move_cost *= obstruction.exit_multiplier;
}
}
}
// TODO: attacks into obstructions are still cheap; is this desirable?
if let Some(target_terrain) = terrain_positions.get(target_pos) {
move_cost += 1.0;
if let Ok(obstruction) = state.world.get::<&Obstruction>(*target_terrain) {
move_cost *= obstruction.entry_multiplier;
}
}
if get_base_terrain(*target_pos).can_enter() {
@ -485,16 +632,16 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
_ => ()
}
if despawn_on_impact.is_some() {
buffer.despawn(entity);
kill(&mut buffer, &mut despawn_buffer, &state, entity, Some(target_entity), Some(*target_pos));
}
if x.0 <= 0.0 {
buffer.despawn(target_entity);
if x.0 < 0.0 {
kill(&mut buffer, &mut despawn_buffer, &state, target_entity, Some(entity), Some(*target_pos));
Some(Entry::Occupied(o))
} else {
None
}
} else {
None // TODO: on pickup or something
None // no "on pickup" exists; emulated with health 0
}
},
Entry::Vacant(v) => Some(Entry::Vacant(v))
@ -503,8 +650,8 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
// TODO: perhaps this should be applied to attacks too?
if consume_energy_if_available(&mut energy, move_cost) {
*entry.or_insert(entity) = entity;
positions.remove(current_pos);
*current_pos = *target_pos;
positions.remove(&current_pos.0.pop_back().unwrap());
current_pos.0.push_front(*target_pos);
}
}
}
@ -518,26 +665,37 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
}
// Process transient entities
for (entity, tick) in state.world.query_mut::<&DespawnOnTick>() {
for (entity, tick) in state.world.query::<&DespawnOnTick>().iter() {
if state.ticks == tick.0 {
buffer.despawn(entity);
kill(&mut buffer, &mut despawn_buffer, &state, entity, None, None);
}
}
for (entity, DespawnRandomly(inv_rate)) in state.world.query_mut::<&DespawnRandomly>() {
for (entity, DespawnRandomly(inv_rate)) in state.world.query::<&DespawnRandomly>().iter() {
if fastrand::u64(0..*inv_rate) == 0 {
buffer.despawn(entity);
kill(&mut buffer, &mut despawn_buffer, &state, entity, None, None);
}
}
buffer.run_on(&mut state.world);
let mut delete = vec![];
for (position, entity) in positions.iter() {
if despawn_buffer.contains(entity) {
delete.push(*position);
}
}
for position in delete {
positions.remove(&position);
}
// Send views to clients
// TODO: terrain layer below others
for (_id, client) in state.clients.iter() {
client.frames_tx.send(Frame::PlayerCount(state.clients.len())).await?;
let mut nearby = vec![];
if let Ok(pos) = state.world.get::<&Position>(client.entity) {
let pos = pos.0;
let pos = pos.head();
for q in -VIEW..=VIEW {
for r in (-VIEW).max(-q - VIEW)..= VIEW.min(-q+VIEW) {
let offset = CoordVec::new(q, r);
@ -548,9 +706,12 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
if let Some(entity) = positions.get(&pos) {
let render = state.world.get::<&Render>(*entity)?;
let health = if let Ok(h) = state.world.get::<&Health>(*entity) {
h.0 / h.1
h.pct()
} else { 1.0 };
nearby.push((q, r, render.0, health))
nearby.push((q, r, render.0, health));
} else if let Some(entity) = terrain_positions.get(&pos) {
let render = state.world.get::<&Render>(*entity)?;
nearby.push((q, r, render.0, 1.0));
} else {
let mut rng = rng_from_hash(pos);
let bg = if rng.usize(0..10) == 0 { ',' } else { '.' };
@ -560,7 +721,9 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
}
}
let health = state.world.get::<&Health>(client.entity)?.0;
client.frames_tx.send(Frame::Display { nearby, health }).await?;
let inventory = state.world.get::<&Inventory>(client.entity)?.0
.iter().map(|(i, q)| (i.name().to_string(), i.description().to_string(), *q)).filter(|(_, _, q)| *q > 0).collect();
client.frames_tx.send(Frame::Display { nearby, health, inventory }).await?;
} else {
client.frames_tx.send(Frame::Dead).await?;
}
@ -599,7 +762,7 @@ fn add_new_player(state: &mut GameState) -> Result<Entity> {
}
};
Ok(state.world.spawn((
Position(pos),
Position::single_tile(pos),
PlayerCharacter,
Render(random_identifier()),
Collidable,
@ -610,7 +773,9 @@ fn add_new_player(state: &mut GameState) -> Result<Entity> {
spawn_range: 3..=10,
spawn_rate_inv: 20,
aggression_range: 5
}
},
Energy { current: 0.0, regeneration_rate: 1.0, burst: 5.0 },
Inventory::empty()
)))
}

View File

@ -387,18 +387,23 @@
var { window: window_1 } = globals;
function get_each_context(ctx, list, i) {
const child_ctx = ctx.slice();
child_ctx[17] = list[i];
child_ctx[19] = i;
child_ctx[18] = list[i];
return child_ctx;
}
function get_each_context_1(ctx, list, i) {
const child_ctx = ctx.slice();
child_ctx[20] = list[i];
child_ctx[21] = list[i];
child_ctx[23] = i;
return child_ctx;
}
function create_each_block_1(ctx) {
function get_each_context_2(ctx, list, i) {
const child_ctx = ctx.slice();
child_ctx[24] = list[i];
return child_ctx;
}
function create_each_block_2(ctx) {
let div;
let t_value = ctx[20][0] + "";
let t_value = ctx[24][0] + "";
let t;
let div_style_value;
return {
@ -406,16 +411,16 @@
div = element("div");
t = text(t_value);
attr(div, "class", "cell svelte-oncm9j");
attr(div, "style", div_style_value = `width: ${ctx[5]}px; height: ${ctx[6]}px; line-height: ${ctx[6]}px; opacity: ${ctx[20][1] * 100}%`);
attr(div, "style", div_style_value = `width: ${ctx[6]}px; height: ${ctx[7]}px; line-height: ${ctx[7]}px; opacity: ${ctx[24][1] * 100}%`);
},
m(target, anchor) {
insert(target, div, anchor);
append(div, t);
},
p(ctx2, dirty) {
if (dirty & 8 && t_value !== (t_value = ctx2[20][0] + ""))
if (dirty & 16 && t_value !== (t_value = ctx2[24][0] + ""))
set_data(t, t_value);
if (dirty & 8 && div_style_value !== (div_style_value = `width: ${ctx2[5]}px; height: ${ctx2[6]}px; line-height: ${ctx2[6]}px; opacity: ${ctx2[20][1] * 100}%`)) {
if (dirty & 16 && div_style_value !== (div_style_value = `width: ${ctx2[6]}px; height: ${ctx2[7]}px; line-height: ${ctx2[7]}px; opacity: ${ctx2[24][1] * 100}%`)) {
attr(div, "style", div_style_value);
}
},
@ -425,14 +430,14 @@
}
};
}
function create_each_block(ctx) {
function create_each_block_1(ctx) {
let div;
let t;
let div_style_value;
let each_value_1 = ctx[17];
let each_value_2 = ctx[21];
let each_blocks = [];
for (let i = 0; i < each_value_1.length; i += 1) {
each_blocks[i] = create_each_block_1(get_each_context_1(ctx, each_value_1, i));
for (let i = 0; i < each_value_2.length; i += 1) {
each_blocks[i] = create_each_block_2(get_each_context_2(ctx, each_value_2, i));
}
return {
c() {
@ -442,7 +447,7 @@
}
t = space();
attr(div, "class", "row svelte-oncm9j");
attr(div, "style", div_style_value = `height: ${ctx[6]}px; ` + (ctx[19] % 2 === 1 ? `padding-left: ${ctx[5] / 2}px` : ""));
attr(div, "style", div_style_value = `height: ${ctx[7]}px; ` + (ctx[23] % 2 === 1 ? `padding-left: ${ctx[6] / 2}px` : ""));
},
m(target, anchor) {
insert(target, div, anchor);
@ -452,15 +457,15 @@
append(div, t);
},
p(ctx2, dirty) {
if (dirty & 104) {
each_value_1 = ctx2[17];
if (dirty & 208) {
each_value_2 = ctx2[21];
let i;
for (i = 0; i < each_value_1.length; i += 1) {
const child_ctx = get_each_context_1(ctx2, each_value_1, i);
for (i = 0; i < each_value_2.length; i += 1) {
const child_ctx = get_each_context_2(ctx2, each_value_2, i);
if (each_blocks[i]) {
each_blocks[i].p(child_ctx, dirty);
} else {
each_blocks[i] = create_each_block_1(child_ctx);
each_blocks[i] = create_each_block_2(child_ctx);
each_blocks[i].c();
each_blocks[i].m(div, t);
}
@ -468,7 +473,7 @@
for (; i < each_blocks.length; i += 1) {
each_blocks[i].d(1);
}
each_blocks.length = each_value_1.length;
each_blocks.length = each_value_2.length;
}
},
d(detaching) {
@ -497,7 +502,7 @@
insert(target, a, anchor);
insert(target, t2, anchor);
if (!mounted) {
dispose = listen(a, "click", ctx[4]);
dispose = listen(a, "click", ctx[5]);
mounted = true;
}
},
@ -567,6 +572,47 @@
}
};
}
function create_each_block(ctx) {
let li;
let t0_value = ctx[18][0] + "";
let t0;
let t1;
let t2_value = ctx[18][2] + "";
let t2;
let t3;
let t4_value = ctx[18][1] + "";
let t4;
return {
c() {
li = element("li");
t0 = text(t0_value);
t1 = text(" x");
t2 = text(t2_value);
t3 = text(": ");
t4 = text(t4_value);
},
m(target, anchor) {
insert(target, li, anchor);
append(li, t0);
append(li, t1);
append(li, t2);
append(li, t3);
append(li, t4);
},
p(ctx2, dirty) {
if (dirty & 8 && t0_value !== (t0_value = ctx2[18][0] + ""))
set_data(t0, t0_value);
if (dirty & 8 && t2_value !== (t2_value = ctx2[18][2] + ""))
set_data(t2, t2_value);
if (dirty & 8 && t4_value !== (t4_value = ctx2[18][1] + ""))
set_data(t4, t4_value);
},
d(detaching) {
if (detaching)
detach(li);
}
};
}
function create_fragment(ctx) {
let h1;
let t1;
@ -576,16 +622,23 @@
let div1;
let t3;
let t4;
let t5;
let ul;
let mounted;
let dispose;
let each_value_1 = ctx[4];
let each_blocks_1 = [];
for (let i = 0; i < each_value_1.length; i += 1) {
each_blocks_1[i] = create_each_block_1(get_each_context_1(ctx, each_value_1, i));
}
let if_block0 = ctx[0] && create_if_block_2(ctx);
let if_block1 = ctx[2] && create_if_block_1(ctx);
let if_block2 = ctx[1] && create_if_block(ctx);
let each_value = ctx[3];
let each_blocks = [];
for (let i = 0; i < each_value.length; i += 1) {
each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i));
}
let if_block0 = ctx[0] && create_if_block_2(ctx);
let if_block1 = ctx[2] && create_if_block_1(ctx);
let if_block2 = ctx[1] && create_if_block(ctx);
return {
c() {
h1 = element("h1");
@ -593,8 +646,8 @@
t1 = space();
div2 = element("div");
div0 = element("div");
for (let i = 0; i < each_blocks.length; i += 1) {
each_blocks[i].c();
for (let i = 0; i < each_blocks_1.length; i += 1) {
each_blocks_1[i].c();
}
t2 = space();
div1 = element("div");
@ -606,6 +659,11 @@
t4 = space();
if (if_block2)
if_block2.c();
t5 = space();
ul = element("ul");
for (let i = 0; i < each_blocks.length; i += 1) {
each_blocks[i].c();
}
attr(div0, "class", "game-display svelte-oncm9j");
attr(div1, "class", "controls");
attr(div2, "class", "wrapper svelte-oncm9j");
@ -615,8 +673,8 @@
insert(target, t1, anchor);
insert(target, div2, anchor);
append(div2, div0);
for (let i = 0; i < each_blocks.length; i += 1) {
each_blocks[i].m(div0, null);
for (let i = 0; i < each_blocks_1.length; i += 1) {
each_blocks_1[i].m(div0, null);
}
append(div2, t2);
append(div2, div1);
@ -628,32 +686,37 @@
append(div1, t4);
if (if_block2)
if_block2.m(div1, null);
append(div1, t5);
append(div1, ul);
for (let i = 0; i < each_blocks.length; i += 1) {
each_blocks[i].m(ul, null);
}
if (!mounted) {
dispose = [
listen(window_1, "keydown", ctx[7]),
listen(window_1, "keyup", ctx[8])
listen(window_1, "keydown", ctx[8]),
listen(window_1, "keyup", ctx[9])
];
mounted = true;
}
},
p(ctx2, [dirty]) {
if (dirty & 104) {
each_value = ctx2[3];
if (dirty & 208) {
each_value_1 = ctx2[4];
let i;
for (i = 0; i < each_value.length; i += 1) {
const child_ctx = get_each_context(ctx2, each_value, i);
if (each_blocks[i]) {
each_blocks[i].p(child_ctx, dirty);
for (i = 0; i < each_value_1.length; i += 1) {
const child_ctx = get_each_context_1(ctx2, each_value_1, i);
if (each_blocks_1[i]) {
each_blocks_1[i].p(child_ctx, dirty);
} else {
each_blocks[i] = create_each_block(child_ctx);
each_blocks[i].c();
each_blocks[i].m(div0, null);
each_blocks_1[i] = create_each_block_1(child_ctx);
each_blocks_1[i].c();
each_blocks_1[i].m(div0, null);
}
}
for (; i < each_blocks.length; i += 1) {
each_blocks[i].d(1);
for (; i < each_blocks_1.length; i += 1) {
each_blocks_1[i].d(1);
}
each_blocks.length = each_value.length;
each_blocks_1.length = each_value_1.length;
}
if (ctx2[0]) {
if (if_block0) {
@ -685,12 +748,30 @@
} else {
if_block2 = create_if_block(ctx2);
if_block2.c();
if_block2.m(div1, null);
if_block2.m(div1, t5);
}
} else if (if_block2) {
if_block2.d(1);
if_block2 = null;
}
if (dirty & 8) {
each_value = ctx2[3];
let i;
for (i = 0; i < each_value.length; i += 1) {
const child_ctx = get_each_context(ctx2, each_value, i);
if (each_blocks[i]) {
each_blocks[i].p(child_ctx, dirty);
} else {
each_blocks[i] = create_each_block(child_ctx);
each_blocks[i].c();
each_blocks[i].m(ul, null);
}
}
for (; i < each_blocks.length; i += 1) {
each_blocks[i].d(1);
}
each_blocks.length = each_value.length;
}
},
i: noop,
o: noop,
@ -701,13 +782,14 @@
detach(t1);
if (detaching)
detach(div2);
destroy_each(each_blocks, detaching);
destroy_each(each_blocks_1, detaching);
if (if_block0)
if_block0.d();
if (if_block1)
if_block1.d();
if (if_block2)
if_block2.d();
destroy_each(each_blocks, detaching);
mounted = false;
run_all(dispose);
}
@ -719,6 +801,7 @@
let dead = false;
let health;
let players;
let inventory = [];
let ws;
const connect = () => {
ws = new WebSocket(window.location.protocol === "https:" ? "wss://ewo.osmarks.net/" : "ws://localhost:8080/");
@ -731,8 +814,9 @@
const row = r;
newGrid[row + OFFSET][col + OFFSET] = [c, o];
}
$$invalidate(3, grid = newGrid);
$$invalidate(4, grid = newGrid);
$$invalidate(1, health = data.Display.health);
$$invalidate(3, inventory = data.Display.inventory);
}
if (data === "Dead") {
$$invalidate(0, dead = true);
@ -782,10 +866,11 @@
"a": "Left",
"d": "Right",
"z": "DownLeft",
"x": "DownRight"
"x": "DownRight",
"f": "Dig"
};
connect();
return [dead, health, players, grid, restart, HORIZ, VERT, keydown, keyup];
return [dead, health, players, inventory, grid, restart, HORIZ, VERT, keydown, keyup];
}
var App = class extends SvelteComponent {
constructor(options) {