1
0
mirror of https://github.com/osmarks/ewo3.git synced 2025-01-04 22: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", "crypto-common",
] ]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "euclid" name = "euclid"
version = "0.22.10" version = "0.22.10"
@ -149,6 +155,7 @@ dependencies = [
"fastrand", "fastrand",
"futures-util", "futures-util",
"hecs", "hecs",
"indexmap",
"lazy_static", "lazy_static",
"noise-functions", "noise-functions",
"seahash", "seahash",
@ -286,6 +293,16 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.11"

View File

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

View File

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

View File

@ -1,12 +1,13 @@
use hecs::{Entity, World}; use hecs::{CommandBuffer, Entity, World};
use euclid::{Point3D, Point2D, Vector2D}; use euclid::{Point3D, Point2D, Vector2D};
use futures_util::{stream::TryStreamExt, SinkExt, StreamExt}; use futures_util::{stream::TryStreamExt, SinkExt, StreamExt};
use indexmap::IndexMap;
use noise_functions::Sample3; use noise_functions::Sample3;
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::tungstenite::protocol::Message; use tokio_tungstenite::tungstenite::protocol::Message;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use anyhow::{Result, Context, anyhow}; 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 slab::Slab;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -90,8 +91,9 @@ enum Input {
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
enum Frame { enum Frame {
Dead, Dead,
Display { nearby: Vec<(i64, i64, char, f32)>, health: f32 }, Display { nearby: Vec<(i64, i64, char, f32)>, health: f32, inventory: Vec<(String, String, u64)> },
PlayerCount(usize) PlayerCount(usize),
Message(String)
} }
struct Client { struct Client {
@ -106,16 +108,45 @@ struct GameState {
ticks: u64 ticks: u64
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Item { 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)] #[derive(Debug, Clone)]
struct PlayerCharacter; struct PlayerCharacter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Position(Coord); 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct MovingInto(Coord); struct MovingInto(Coord);
@ -123,6 +154,13 @@ struct MovingInto(Coord);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Health(f32, f32); 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)] #[derive(Debug, Clone)]
struct Render(char); struct Render(char);
@ -153,20 +191,17 @@ struct Collidable;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Velocity(CoordVec); struct Velocity(CoordVec);
#[derive(Debug, Clone)]
struct DeferredRandomly<T: Clone + std::fmt::Debug + hecs::Bundle>(u64, T);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Terrain; struct Terrain;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Obstruction { entry_cost: StochasticNumber, exit_cost: StochasticNumber } struct Obstruction { entry_multiplier: f32, exit_multiplier: f32 }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Energy { current: f32, regeneration_rate: f32, burst: f32 } struct Energy { current: f32, regeneration_rate: f32, burst: f32 }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct DespawnOnImpact; struct Drops(Vec<(Item, StochasticNumber)>);
impl Energy { impl Energy {
fn try_consume(&mut self, cost: f32) -> bool { 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 VIEW: i64 = 15;
const WALL: i64 = 128; const WALL: i64 = 128;
const RANDOM_DESPAWN_INV_RATE: u64 = 4000; const RANDOM_DESPAWN_INV_RATE: u64 = 4000;
@ -239,21 +314,22 @@ struct EnemySpec {
initial_health: f32, initial_health: f32,
move_delay: usize, move_delay: usize,
attack_cooldown: u64, attack_cooldown: u64,
ranged: bool ranged: bool,
drops: Vec<(Item, StochasticNumber)>
} }
impl EnemySpec { impl EnemySpec {
// Numbers ported from original EWO. Fudge constants added elsewhere. // Numbers ported from original EWO. Fudge constants added elsewhere.
fn random() -> EnemySpec { fn random() -> EnemySpec {
match fastrand::usize(0..650) { 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 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 }, // KESTREL 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 }, // SNAKE 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 }, // EMU 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 }, // OGRE 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 }, // RAT 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 }, // MOA 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 }, // PLATYPUS 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!() _ => 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 { fn triangle_from_min_range(min: f32, range: f32) -> Self {
StochasticNumber::Triangle { min: min, max: min + range, mode: (min + range) / 2.0 } 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(); let mut positions = HashMap::new();
for (entity, pos) in state.world.query_mut::<hecs::With<&Position, &Collidable>>() { 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>>() { 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(); let mut buffer = hecs::CommandBuffer::new();
// Spawn enemies // 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 { if fastrand::usize(0..*spawn_rate_inv) == 0 {
let c = count_hexes(*spawn_range.end()); 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; let mut occupied = false;
for _ in 0..(c as f32 / spawn_density * 0.005).ceil() as usize { for _ in 0..(c as f32 / spawn_density * 0.005).ceil() as usize {
if positions.contains_key(&newpos) { if positions.contains_key(&newpos) {
occupied = true; occupied = true;
break; 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() { if !occupied && get_base_terrain(newpos).can_enter() && hex_distance(newpos, pos) >= *spawn_range.start() {
let spec = EnemySpec::random(); 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 { if spec.ranged {
buffer.spawn(( buffer.spawn((
Render(spec.symbol), Render(spec.symbol),
Health(spec.initial_health, spec.initial_health), Health(spec.initial_health, spec.initial_health),
Enemy, Enemy,
RangedAttack { damage: StochasticNumber::triangle_from_min_range(spec.min_damage, spec.damage_range), energy: spec.attack_cooldown as f32, range: 4 }, 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 }), 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, Collidable,
DespawnRandomly(RANDOM_DESPAWN_INV_RATE), 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 { } else {
buffer.spawn(( buffer.spawn((
@ -360,11 +447,12 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Health(spec.initial_health, spec.initial_health), Health(spec.initial_health, spec.initial_health),
Enemy, Enemy,
Attack { damage: StochasticNumber::triangle_from_min_range(spec.min_damage, spec.damage_range), energy: spec.attack_cooldown as f32 }, 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 }), 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, Collidable,
DespawnRandomly(RANDOM_DESPAWN_INV_RATE), 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 // 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() { 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) { if let Ok(_) = state.world.get::<&EnemyTarget>(*target) {
buffer.insert_one(entity, MovingInto(*pos + *direction)); buffer.insert_one(entity, MovingInto(pos + *direction));
continue; continue;
} }
} }
@ -386,11 +476,12 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
// TODO we maybe need a spatial index for this // TODO we maybe need a spatial index for this
for (_entity, (target_pos, target)) in state.world.query::<(&Position, &EnemyTarget)>().iter() { 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 { if distance < target.aggression_range {
match closest { match closest {
Some((_pos, old_distance)) if old_distance < distance => closest = Some((target_pos.0, distance)), Some((_pos, old_distance)) if old_distance < distance => closest = Some((target_pos, distance)),
None => closest = Some((target_pos.0, 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 { if let Some(ranged_attack) = ranged {
// slightly smart behaviour for ranged attacker: try to stay just within range // slightly smart behaviour for ranged attacker: try to stay just within range
let direction = DIRECTIONS.iter().min_by_key(|dir| let direction = DIRECTIONS.iter().min_by_key(|dir|
(hex_distance(*pos + **dir, target_pos) - (ranged_attack.range as i64 - 1)).abs()).unwrap(); (hex_distance(pos + **dir, target_pos) - (ranged_attack.range as i64 - 1)).abs()).unwrap();
buffer.insert_one(entity, MovingInto(*pos + *direction)); buffer.insert_one(entity, MovingInto(pos + *direction));
// do ranged attack if valid // 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)) { 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()); let atk_dir = atk_dir.clamp(-CoordVec::one(), CoordVec::one());
buffer.spawn(( buffer.spawn((
@ -411,23 +502,23 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Enemy, Enemy,
Attack { damage: ranged_attack.damage, energy: 0.0 }, Attack { damage: ranged_attack.damage, energy: 0.0 },
Velocity(atk_dir), Velocity(atk_dir),
Position(*pos), Position::single_tile(pos),
DespawnOnTick(state.ticks.wrapping_add(ranged_attack.range)) DespawnOnTick(state.ticks.wrapping_add(ranged_attack.range))
)); ));
} }
} else { } else {
let direction = DIRECTIONS.iter().min_by_key(|dir| hex_distance(*pos + **dir, target_pos)).unwrap(); let direction = DIRECTIONS.iter().min_by_key(|dir| hex_distance(pos + **dir, target_pos)).unwrap();
buffer.insert_one(entity, MovingInto(*pos + *direction)); buffer.insert_one(entity, MovingInto(pos + *direction));
} }
} else { } else {
// wander randomly (ethical) // 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 // Process velocity
for (entity, (Position(pos), Velocity(vel))) in state.world.query_mut::<(&Position, &Velocity)>() { for (entity, (pos, Velocity(vel))) in state.world.query_mut::<(&Position, &Velocity)>() {
buffer.insert_one(entity, MovingInto(*pos + *vel)); buffer.insert_one(entity, MovingInto(pos.head() + *vel));
} }
buffer.run_on(&mut state.world); buffer.run_on(&mut state.world);
@ -435,6 +526,9 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
// Process inputs // Process inputs
for (_id, client) in state.clients.iter_mut() { for (_id, client) in state.clients.iter_mut() {
let mut next_movement = CoordVec::zero(); 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 { loop {
let recv = client.inputs_rx.try_recv(); let recv = client.inputs_rx.try_recv();
match recv { match recv {
@ -447,28 +541,81 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Input::DownRight => next_movement = CoordVec::new(0, 1), Input::DownRight => next_movement = CoordVec::new(0, 1),
Input::DownLeft => next_movement = CoordVec::new(-1, 1), Input::DownLeft => next_movement = CoordVec::new(-1, 1),
Input::Dig => { 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()) Err(e) => return Err(e.into())
} }
} }
let position = state.world.get::<&mut Position>(client.entity)?.0;
let target = position + next_movement; let target = position + next_movement;
if get_base_terrain(target).can_enter() && target != position { 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 buffer.run_on(&mut state.world);
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); let mut despawn_buffer = HashSet::new();
if let Some(current_terrain) = terrain_positions.get(current_pos) {
move_cost += 1.0; // 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) { 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() { 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() { 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 { if x.0 < 0.0 {
buffer.despawn(target_entity); kill(&mut buffer, &mut despawn_buffer, &state, target_entity, Some(entity), Some(*target_pos));
Some(Entry::Occupied(o)) Some(Entry::Occupied(o))
} else { } else {
None None
} }
} else { } else {
None // TODO: on pickup or something None // no "on pickup" exists; emulated with health 0
} }
}, },
Entry::Vacant(v) => Some(Entry::Vacant(v)) 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? // TODO: perhaps this should be applied to attacks too?
if consume_energy_if_available(&mut energy, move_cost) { if consume_energy_if_available(&mut energy, move_cost) {
*entry.or_insert(entity) = entity; *entry.or_insert(entity) = entity;
positions.remove(current_pos); positions.remove(&current_pos.0.pop_back().unwrap());
*current_pos = *target_pos; current_pos.0.push_front(*target_pos);
} }
} }
} }
@ -518,26 +665,37 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
} }
// Process transient entities // 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 { 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 { 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); 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 // Send views to clients
// TODO: terrain layer below others
for (_id, client) in state.clients.iter() { for (_id, client) in state.clients.iter() {
client.frames_tx.send(Frame::PlayerCount(state.clients.len())).await?; client.frames_tx.send(Frame::PlayerCount(state.clients.len())).await?;
let mut nearby = vec![]; let mut nearby = vec![];
if let Ok(pos) = state.world.get::<&Position>(client.entity) { if let Ok(pos) = state.world.get::<&Position>(client.entity) {
let pos = pos.0; let pos = pos.head();
for q in -VIEW..=VIEW { for q in -VIEW..=VIEW {
for r in (-VIEW).max(-q - VIEW)..= VIEW.min(-q+VIEW) { for r in (-VIEW).max(-q - VIEW)..= VIEW.min(-q+VIEW) {
let offset = CoordVec::new(q, r); 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) { if let Some(entity) = positions.get(&pos) {
let render = state.world.get::<&Render>(*entity)?; let render = state.world.get::<&Render>(*entity)?;
let health = if let Ok(h) = state.world.get::<&Health>(*entity) { let health = if let Ok(h) = state.world.get::<&Health>(*entity) {
h.0 / h.1 h.pct()
} else { 1.0 }; } 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 { } else {
let mut rng = rng_from_hash(pos); let mut rng = rng_from_hash(pos);
let bg = if rng.usize(0..10) == 0 { ',' } else { '.' }; 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; 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 { } else {
client.frames_tx.send(Frame::Dead).await?; client.frames_tx.send(Frame::Dead).await?;
} }
@ -599,7 +762,7 @@ fn add_new_player(state: &mut GameState) -> Result<Entity> {
} }
}; };
Ok(state.world.spawn(( Ok(state.world.spawn((
Position(pos), Position::single_tile(pos),
PlayerCharacter, PlayerCharacter,
Render(random_identifier()), Render(random_identifier()),
Collidable, Collidable,
@ -610,7 +773,9 @@ fn add_new_player(state: &mut GameState) -> Result<Entity> {
spawn_range: 3..=10, spawn_range: 3..=10,
spawn_rate_inv: 20, spawn_rate_inv: 20,
aggression_range: 5 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; var { window: window_1 } = globals;
function get_each_context(ctx, list, i) { function get_each_context(ctx, list, i) {
const child_ctx = ctx.slice(); const child_ctx = ctx.slice();
child_ctx[17] = list[i]; child_ctx[18] = list[i];
child_ctx[19] = i;
return child_ctx; return child_ctx;
} }
function get_each_context_1(ctx, list, i) { function get_each_context_1(ctx, list, i) {
const child_ctx = ctx.slice(); const child_ctx = ctx.slice();
child_ctx[20] = list[i]; child_ctx[21] = list[i];
child_ctx[23] = i;
return child_ctx; 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 div;
let t_value = ctx[20][0] + ""; let t_value = ctx[24][0] + "";
let t; let t;
let div_style_value; let div_style_value;
return { return {
@ -406,16 +411,16 @@
div = element("div"); div = element("div");
t = text(t_value); t = text(t_value);
attr(div, "class", "cell svelte-oncm9j"); 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) { m(target, anchor) {
insert(target, div, anchor); insert(target, div, anchor);
append(div, t); append(div, t);
}, },
p(ctx2, dirty) { 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); 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); attr(div, "style", div_style_value);
} }
}, },
@ -425,14 +430,14 @@
} }
}; };
} }
function create_each_block(ctx) { function create_each_block_1(ctx) {
let div; let div;
let t; let t;
let div_style_value; let div_style_value;
let each_value_1 = ctx[17]; let each_value_2 = ctx[21];
let each_blocks = []; let each_blocks = [];
for (let i = 0; i < each_value_1.length; i += 1) { for (let i = 0; i < each_value_2.length; i += 1) {
each_blocks[i] = create_each_block_1(get_each_context_1(ctx, each_value_1, i)); each_blocks[i] = create_each_block_2(get_each_context_2(ctx, each_value_2, i));
} }
return { return {
c() { c() {
@ -442,7 +447,7 @@
} }
t = space(); t = space();
attr(div, "class", "row svelte-oncm9j"); 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) { m(target, anchor) {
insert(target, div, anchor); insert(target, div, anchor);
@ -452,15 +457,15 @@
append(div, t); append(div, t);
}, },
p(ctx2, dirty) { p(ctx2, dirty) {
if (dirty & 104) { if (dirty & 208) {
each_value_1 = ctx2[17]; each_value_2 = ctx2[21];
let i; let i;
for (i = 0; i < each_value_1.length; i += 1) { for (i = 0; i < each_value_2.length; i += 1) {
const child_ctx = get_each_context_1(ctx2, each_value_1, i); const child_ctx = get_each_context_2(ctx2, each_value_2, i);
if (each_blocks[i]) { if (each_blocks[i]) {
each_blocks[i].p(child_ctx, dirty); each_blocks[i].p(child_ctx, dirty);
} else { } 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].c();
each_blocks[i].m(div, t); each_blocks[i].m(div, t);
} }
@ -468,7 +473,7 @@
for (; i < each_blocks.length; i += 1) { for (; i < each_blocks.length; i += 1) {
each_blocks[i].d(1); each_blocks[i].d(1);
} }
each_blocks.length = each_value_1.length; each_blocks.length = each_value_2.length;
} }
}, },
d(detaching) { d(detaching) {
@ -497,7 +502,7 @@
insert(target, a, anchor); insert(target, a, anchor);
insert(target, t2, anchor); insert(target, t2, anchor);
if (!mounted) { if (!mounted) {
dispose = listen(a, "click", ctx[4]); dispose = listen(a, "click", ctx[5]);
mounted = true; 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) { function create_fragment(ctx) {
let h1; let h1;
let t1; let t1;
@ -576,16 +622,23 @@
let div1; let div1;
let t3; let t3;
let t4; let t4;
let t5;
let ul;
let mounted; let mounted;
let dispose; 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_value = ctx[3];
let each_blocks = []; let each_blocks = [];
for (let i = 0; i < each_value.length; i += 1) { for (let i = 0; i < each_value.length; i += 1) {
each_blocks[i] = create_each_block(get_each_context(ctx, each_value, i)); 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 { return {
c() { c() {
h1 = element("h1"); h1 = element("h1");
@ -593,8 +646,8 @@
t1 = space(); t1 = space();
div2 = element("div"); div2 = element("div");
div0 = element("div"); div0 = element("div");
for (let i = 0; i < each_blocks.length; i += 1) { for (let i = 0; i < each_blocks_1.length; i += 1) {
each_blocks[i].c(); each_blocks_1[i].c();
} }
t2 = space(); t2 = space();
div1 = element("div"); div1 = element("div");
@ -606,6 +659,11 @@
t4 = space(); t4 = space();
if (if_block2) if (if_block2)
if_block2.c(); 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(div0, "class", "game-display svelte-oncm9j");
attr(div1, "class", "controls"); attr(div1, "class", "controls");
attr(div2, "class", "wrapper svelte-oncm9j"); attr(div2, "class", "wrapper svelte-oncm9j");
@ -615,8 +673,8 @@
insert(target, t1, anchor); insert(target, t1, anchor);
insert(target, div2, anchor); insert(target, div2, anchor);
append(div2, div0); append(div2, div0);
for (let i = 0; i < each_blocks.length; i += 1) { for (let i = 0; i < each_blocks_1.length; i += 1) {
each_blocks[i].m(div0, null); each_blocks_1[i].m(div0, null);
} }
append(div2, t2); append(div2, t2);
append(div2, div1); append(div2, div1);
@ -628,32 +686,37 @@
append(div1, t4); append(div1, t4);
if (if_block2) if (if_block2)
if_block2.m(div1, null); 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) { if (!mounted) {
dispose = [ dispose = [
listen(window_1, "keydown", ctx[7]), listen(window_1, "keydown", ctx[8]),
listen(window_1, "keyup", ctx[8]) listen(window_1, "keyup", ctx[9])
]; ];
mounted = true; mounted = true;
} }
}, },
p(ctx2, [dirty]) { p(ctx2, [dirty]) {
if (dirty & 104) { if (dirty & 208) {
each_value = ctx2[3]; each_value_1 = ctx2[4];
let i; let i;
for (i = 0; i < each_value.length; i += 1) { for (i = 0; i < each_value_1.length; i += 1) {
const child_ctx = get_each_context(ctx2, each_value, i); const child_ctx = get_each_context_1(ctx2, each_value_1, i);
if (each_blocks[i]) { if (each_blocks_1[i]) {
each_blocks[i].p(child_ctx, dirty); each_blocks_1[i].p(child_ctx, dirty);
} else { } else {
each_blocks[i] = create_each_block(child_ctx); each_blocks_1[i] = create_each_block_1(child_ctx);
each_blocks[i].c(); each_blocks_1[i].c();
each_blocks[i].m(div0, null); each_blocks_1[i].m(div0, null);
} }
} }
for (; i < each_blocks.length; i += 1) { for (; i < each_blocks_1.length; i += 1) {
each_blocks[i].d(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 (ctx2[0]) {
if (if_block0) { if (if_block0) {
@ -685,12 +748,30 @@
} else { } else {
if_block2 = create_if_block(ctx2); if_block2 = create_if_block(ctx2);
if_block2.c(); if_block2.c();
if_block2.m(div1, null); if_block2.m(div1, t5);
} }
} else if (if_block2) { } else if (if_block2) {
if_block2.d(1); if_block2.d(1);
if_block2 = null; 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, i: noop,
o: noop, o: noop,
@ -701,13 +782,14 @@
detach(t1); detach(t1);
if (detaching) if (detaching)
detach(div2); detach(div2);
destroy_each(each_blocks, detaching); destroy_each(each_blocks_1, detaching);
if (if_block0) if (if_block0)
if_block0.d(); if_block0.d();
if (if_block1) if (if_block1)
if_block1.d(); if_block1.d();
if (if_block2) if (if_block2)
if_block2.d(); if_block2.d();
destroy_each(each_blocks, detaching);
mounted = false; mounted = false;
run_all(dispose); run_all(dispose);
} }
@ -719,6 +801,7 @@
let dead = false; let dead = false;
let health; let health;
let players; let players;
let inventory = [];
let ws; let ws;
const connect = () => { const connect = () => {
ws = new WebSocket(window.location.protocol === "https:" ? "wss://ewo.osmarks.net/" : "ws://localhost:8080/"); ws = new WebSocket(window.location.protocol === "https:" ? "wss://ewo.osmarks.net/" : "ws://localhost:8080/");
@ -731,8 +814,9 @@
const row = r; const row = r;
newGrid[row + OFFSET][col + OFFSET] = [c, o]; newGrid[row + OFFSET][col + OFFSET] = [c, o];
} }
$$invalidate(3, grid = newGrid); $$invalidate(4, grid = newGrid);
$$invalidate(1, health = data.Display.health); $$invalidate(1, health = data.Display.health);
$$invalidate(3, inventory = data.Display.inventory);
} }
if (data === "Dead") { if (data === "Dead") {
$$invalidate(0, dead = true); $$invalidate(0, dead = true);
@ -782,10 +866,11 @@
"a": "Left", "a": "Left",
"d": "Right", "d": "Right",
"z": "DownLeft", "z": "DownLeft",
"x": "DownRight" "x": "DownRight",
"f": "Dig"
}; };
connect(); 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 { var App = class extends SvelteComponent {
constructor(options) { constructor(options) {