@crowdedkingdoms/ckx-kit 0.2.0 → 0.4.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/crates/ckx-sdk/Cargo.toml +1 -1
- package/crates/ckx-sdk/src/kit/ai/brain.rs +288 -0
- package/crates/ckx-sdk/src/kit/ai/flow.rs +163 -0
- package/crates/ckx-sdk/src/kit/ai/mod.rs +10 -0
- package/crates/ckx-sdk/src/kit/ai/path.rs +214 -0
- package/crates/ckx-sdk/src/kit/ai/steering.rs +137 -0
- package/crates/ckx-sdk/src/kit/ai/turn.rs +141 -0
- package/crates/ckx-sdk/src/kit/mod.rs +542 -0
- package/crates/ckx-sdk/src/kit/play/abilities.rs +370 -0
- package/crates/ckx-sdk/src/kit/play/cards.rs +220 -0
- package/crates/ckx-sdk/src/kit/play/combat.rs +292 -0
- package/crates/ckx-sdk/src/kit/play/mod.rs +15 -0
- package/crates/ckx-sdk/src/kit/play/movement.rs +196 -0
- package/crates/ckx-sdk/src/kit/play/score.rs +183 -0
- package/crates/ckx-sdk/src/kit/play/timing.rs +218 -0
- package/crates/ckx-sdk/src/kit/play/turns.rs +315 -0
- package/crates/ckx-sdk/src/kit/presence.rs +142 -0
- package/crates/ckx-sdk/src/kit/rng.rs +137 -0
- package/crates/ckx-sdk/src/kit/shard.rs +65 -0
- package/crates/ckx-sdk/src/kit/sim/daycycle.rs +71 -0
- package/crates/ckx-sdk/src/kit/sim/mod.rs +10 -0
- package/crates/ckx-sdk/src/kit/sim/nodes.rs +109 -0
- package/crates/ckx-sdk/src/kit/sim/waves.rs +105 -0
- package/crates/ckx-sdk/src/kit/sim/weather.rs +142 -0
- package/crates/ckx-sdk/src/kit/sim/zones.rs +218 -0
- package/crates/ckx-sdk/src/kit/wire.rs +266 -0
- package/crates/ckx-sdk/src/lib.rs +13 -0
- package/crates/ckx-sdk/src/model.rs +5 -0
- package/crates/crowdy-client-sdk/Cargo.toml +1 -1
- package/dist/cli/src/new.js +9 -0
- package/kit.pin +4 -4
- package/package.json +1 -1
- package/starters/arena/Cargo.toml +14 -0
- package/starters/arena/src/lib.rs +849 -0
- package/starters/ckx.json +12 -1
- package/starters/deck/Cargo.toml +14 -0
- package/starters/deck/src/lib.rs +510 -0
- package/starters/director/Cargo.toml +14 -0
- package/starters/director/src/lib.rs +628 -0
- package/starters/grid-mod/Cargo.toml +1 -1
- package/starters/instance/Cargo.toml +14 -0
- package/starters/instance/src/lib.rs +656 -0
- package/starters/leaderboard/Cargo.toml +14 -0
- package/starters/leaderboard/src/lib.rs +395 -0
- package/starters/matchmaker/Cargo.toml +1 -1
- package/starters/matchmaker/src/lib.rs +334 -74
- package/starters/minigame/Cargo.toml +14 -0
- package/starters/minigame/src/lib.rs +391 -0
- package/starters/npc-mobs/Cargo.toml +1 -1
- package/starters/npc-mobs/src/lib.rs +277 -67
- package/starters/racing/Cargo.toml +14 -0
- package/starters/racing/src/lib.rs +1151 -0
- package/starters/session/Cargo.toml +1 -1
- package/starters/session/src/lib.rs +191 -86
- package/starters/tactics/Cargo.toml +14 -0
- package/starters/tactics/src/lib.rs +562 -0
- package/starters/territory/Cargo.toml +14 -0
- package/starters/territory/src/lib.rs +878 -0
- package/starters/world-tick/Cargo.toml +1 -1
- package/starters/world-tick/src/lib.rs +151 -38
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
//! Rule zones: static axis-aligned boxes tagged with rule strings ("safe",
|
|
2
|
+
//! "pvp", "build-locked") plus **shrinking circles** — the
|
|
3
|
+
//! battle-royale zone: a phased schedule of (radius, hold, shrink) steps
|
|
4
|
+
//! with warning/shrink/settle events for the type-98 lane.
|
|
5
|
+
|
|
6
|
+
use serde::{Deserialize, Serialize};
|
|
7
|
+
|
|
8
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
9
|
+
pub struct Zone {
|
|
10
|
+
pub id: String,
|
|
11
|
+
pub rule: String,
|
|
12
|
+
pub min: (f32, f32, f32),
|
|
13
|
+
pub max: (f32, f32, f32),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
impl Zone {
|
|
17
|
+
pub fn contains(&self, pos: (f32, f32, f32)) -> bool {
|
|
18
|
+
pos.0 >= self.min.0
|
|
19
|
+
&& pos.0 <= self.max.0
|
|
20
|
+
&& pos.1 >= self.min.1
|
|
21
|
+
&& pos.1 <= self.max.1
|
|
22
|
+
&& pos.2 >= self.min.2
|
|
23
|
+
&& pos.2 <= self.max.2
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
|
28
|
+
pub struct ZoneSet {
|
|
29
|
+
pub zones: Vec<Zone>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl ZoneSet {
|
|
33
|
+
/// All zones containing the position.
|
|
34
|
+
pub fn at(&self, pos: (f32, f32, f32)) -> Vec<&Zone> {
|
|
35
|
+
self.zones.iter().filter(|z| z.contains(pos)).collect()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Does any zone with this rule cover the position?
|
|
39
|
+
pub fn has_rule(&self, pos: (f32, f32, f32), rule: &str) -> bool {
|
|
40
|
+
self.zones.iter().any(|z| z.rule == rule && z.contains(pos))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Shrinking circles (BR zones)
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/// One phase of a shrinking-zone schedule.
|
|
49
|
+
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
|
|
50
|
+
pub struct ShrinkPhase {
|
|
51
|
+
/// The radius this phase shrinks TO.
|
|
52
|
+
pub radius: f32,
|
|
53
|
+
/// How long the zone HOLDS before shrinking (the warning window).
|
|
54
|
+
pub hold_ms: u64,
|
|
55
|
+
/// How long the shrink itself takes (radius interpolates linearly).
|
|
56
|
+
pub shrink_ms: u64,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Announcements a shrinking zone produces as time advances.
|
|
60
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
61
|
+
pub enum ZoneEvent {
|
|
62
|
+
/// A hold began: the zone will start shrinking to `to_radius` at `at_ms`.
|
|
63
|
+
Warning { phase: usize, to_radius: f32, shrink_starts_at_ms: u64 },
|
|
64
|
+
/// The shrink began.
|
|
65
|
+
Shrinking { phase: usize, to_radius: f32 },
|
|
66
|
+
/// The phase's target radius was reached.
|
|
67
|
+
Settled { phase: usize, radius: f32 },
|
|
68
|
+
/// Every phase is done (the final circle).
|
|
69
|
+
Final { radius: f32 },
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// A battle-royale circle: fixed center, radius stepping down a schedule.
|
|
73
|
+
/// Pure clock math — feed it `now_ms`, read `radius()` and drain events.
|
|
74
|
+
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
|
75
|
+
pub struct ShrinkingZone {
|
|
76
|
+
pub center: (f32, f32),
|
|
77
|
+
pub initial_radius: f32,
|
|
78
|
+
pub phases: Vec<ShrinkPhase>,
|
|
79
|
+
pub started_at_ms: u64,
|
|
80
|
+
/// Last phase-progress marker events were emitted for (phase, stage).
|
|
81
|
+
announced: Vec<(usize, u8)>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl ShrinkingZone {
|
|
85
|
+
pub fn start(center: (f32, f32), initial_radius: f32, phases: Vec<ShrinkPhase>, now_ms: u64) -> Self {
|
|
86
|
+
Self { center, initial_radius, phases, started_at_ms: now_ms, announced: Vec::new() }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Phase timeline offsets: each phase = hold then shrink.
|
|
90
|
+
fn phase_bounds(&self, phase: usize) -> (u64, u64, u64) {
|
|
91
|
+
let mut offset = 0u64;
|
|
92
|
+
for p in &self.phases[..phase] {
|
|
93
|
+
offset += p.hold_ms + p.shrink_ms;
|
|
94
|
+
}
|
|
95
|
+
let p = &self.phases[phase];
|
|
96
|
+
(offset, offset + p.hold_ms, offset + p.hold_ms + p.shrink_ms)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// The circle's radius at `now_ms` (linear interpolation during shrinks).
|
|
100
|
+
pub fn radius(&self, now_ms: u64) -> f32 {
|
|
101
|
+
let elapsed = now_ms.saturating_sub(self.started_at_ms);
|
|
102
|
+
let mut from = self.initial_radius;
|
|
103
|
+
for (index, phase) in self.phases.iter().enumerate() {
|
|
104
|
+
let (_, shrink_start, shrink_end) = self.phase_bounds(index);
|
|
105
|
+
if elapsed < shrink_start {
|
|
106
|
+
return from;
|
|
107
|
+
}
|
|
108
|
+
if elapsed < shrink_end {
|
|
109
|
+
let t = (elapsed - shrink_start) as f32 / (shrink_end - shrink_start).max(1) as f32;
|
|
110
|
+
return from + (phase.radius - from) * t;
|
|
111
|
+
}
|
|
112
|
+
from = phase.radius;
|
|
113
|
+
}
|
|
114
|
+
from
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Is a position inside the live circle?
|
|
118
|
+
pub fn contains(&self, pos: (f32, f32), now_ms: u64) -> bool {
|
|
119
|
+
let (dx, dz) = (pos.0 - self.center.0, pos.1 - self.center.1);
|
|
120
|
+
(dx * dx + dz * dz).sqrt() <= self.radius(now_ms)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Every phase fully settled?
|
|
124
|
+
pub fn done(&self, now_ms: u64) -> bool {
|
|
125
|
+
match self.phases.len() {
|
|
126
|
+
0 => true,
|
|
127
|
+
n => now_ms.saturating_sub(self.started_at_ms) >= self.phase_bounds(n - 1).2,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Advance the announcer: returns the events newly due at `now_ms`
|
|
132
|
+
/// (warning at hold start, shrinking at shrink start, settled at end,
|
|
133
|
+
/// final once after the last phase).
|
|
134
|
+
pub fn poll(&mut self, now_ms: u64) -> Vec<ZoneEvent> {
|
|
135
|
+
let elapsed = now_ms.saturating_sub(self.started_at_ms);
|
|
136
|
+
let mut events = Vec::new();
|
|
137
|
+
for (index, phase) in self.phases.iter().enumerate() {
|
|
138
|
+
let (hold_start, shrink_start, shrink_end) = self.phase_bounds(index);
|
|
139
|
+
let stages: [(u8, u64); 3] = [(0, hold_start), (1, shrink_start), (2, shrink_end)];
|
|
140
|
+
for (stage, at) in stages {
|
|
141
|
+
if elapsed >= at && !self.announced.contains(&(index, stage)) {
|
|
142
|
+
self.announced.push((index, stage));
|
|
143
|
+
events.push(match stage {
|
|
144
|
+
0 => ZoneEvent::Warning {
|
|
145
|
+
phase: index,
|
|
146
|
+
to_radius: phase.radius,
|
|
147
|
+
shrink_starts_at_ms: self.started_at_ms + shrink_start,
|
|
148
|
+
},
|
|
149
|
+
1 => ZoneEvent::Shrinking { phase: index, to_radius: phase.radius },
|
|
150
|
+
_ => ZoneEvent::Settled { phase: index, radius: phase.radius },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if self.done(now_ms) && !self.announced.contains(&(usize::MAX, 3)) {
|
|
156
|
+
self.announced.push((usize::MAX, 3));
|
|
157
|
+
events.push(ZoneEvent::Final { radius: self.radius(now_ms) });
|
|
158
|
+
}
|
|
159
|
+
events
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[cfg(test)]
|
|
164
|
+
mod tests {
|
|
165
|
+
use super::*;
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn shrinking_zone_interpolates_and_announces() {
|
|
169
|
+
let mut zone = ShrinkingZone::start(
|
|
170
|
+
(0.0, 0.0),
|
|
171
|
+
100.0,
|
|
172
|
+
vec![
|
|
173
|
+
ShrinkPhase { radius: 50.0, hold_ms: 10_000, shrink_ms: 10_000 },
|
|
174
|
+
ShrinkPhase { radius: 10.0, hold_ms: 5_000, shrink_ms: 5_000 },
|
|
175
|
+
],
|
|
176
|
+
1_000,
|
|
177
|
+
);
|
|
178
|
+
// Hold: full radius; the warning fires immediately.
|
|
179
|
+
let events = zone.poll(1_000);
|
|
180
|
+
assert!(matches!(events[0], ZoneEvent::Warning { phase: 0, to_radius, .. } if to_radius == 50.0));
|
|
181
|
+
assert_eq!(zone.radius(5_000), 100.0);
|
|
182
|
+
// Mid-shrink: halfway between 100 and 50.
|
|
183
|
+
let events = zone.poll(16_000);
|
|
184
|
+
assert!(events.iter().any(|e| matches!(e, ZoneEvent::Shrinking { phase: 0, .. })));
|
|
185
|
+
assert!((zone.radius(16_000) - 75.0).abs() < 0.01);
|
|
186
|
+
// Settled at 50, phase 1 warning issued in the same poll window.
|
|
187
|
+
let events = zone.poll(21_500);
|
|
188
|
+
assert!(events.iter().any(|e| matches!(e, ZoneEvent::Settled { phase: 0, radius } if *radius == 50.0)));
|
|
189
|
+
assert!(events.iter().any(|e| matches!(e, ZoneEvent::Warning { phase: 1, .. })));
|
|
190
|
+
assert_eq!(zone.radius(24_000), 50.0);
|
|
191
|
+
// Final circle.
|
|
192
|
+
let events = zone.poll(31_000);
|
|
193
|
+
assert!(events.iter().any(|e| matches!(e, ZoneEvent::Settled { phase: 1, radius } if *radius == 10.0)));
|
|
194
|
+
assert!(events.iter().any(|e| matches!(e, ZoneEvent::Final { radius } if *radius == 10.0)));
|
|
195
|
+
assert!(zone.done(31_000));
|
|
196
|
+
assert!(zone.poll(40_000).is_empty(), "announce once");
|
|
197
|
+
// Containment tracks the live radius.
|
|
198
|
+
assert!(zone.contains((60.0, 0.0), 5_000));
|
|
199
|
+
assert!(!zone.contains((60.0, 0.0), 31_000));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn containment_and_rules() {
|
|
204
|
+
let set = ZoneSet {
|
|
205
|
+
zones: vec![
|
|
206
|
+
Zone { id: "spawn".into(), rule: "safe".into(), min: (-10.0, 0.0, -10.0), max: (10.0, 32.0, 10.0) },
|
|
207
|
+
Zone { id: "arena".into(), rule: "pvp".into(), min: (5.0, 0.0, 5.0), max: (30.0, 32.0, 30.0) },
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
assert!(set.has_rule((0.0, 16.0, 0.0), "safe"));
|
|
211
|
+
assert!(!set.has_rule((0.0, 16.0, 0.0), "pvp"));
|
|
212
|
+
// Overlap region carries both rules.
|
|
213
|
+
assert_eq!(set.at((7.0, 16.0, 7.0)).len(), 2);
|
|
214
|
+
// Boundaries are inclusive; outside is outside.
|
|
215
|
+
assert!(set.has_rule((10.0, 32.0, 10.0), "safe"));
|
|
216
|
+
assert!(set.at((50.0, 16.0, 50.0)).is_empty());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
//! What kit hubs show players near them through the realtime tier ([`crate::emit`]): server
|
|
2
|
+
//! events, numbered by the registry below, and the state of server-driven actors.
|
|
3
|
+
//!
|
|
4
|
+
//! A server event's payload is `[event type, u16 little endian][JSON]`. The bodies here use
|
|
5
|
+
//! camelCase keys, the shape the kit clients' event parsers read. An app's own events take
|
|
6
|
+
//! numbers from [`APP_EVENTS`] up.
|
|
7
|
+
//!
|
|
8
|
+
//! A server-driven actor's state is [`ActorState`] as MessagePack: the kit clients decode it
|
|
9
|
+
//! into their actor stores like a player's.
|
|
10
|
+
|
|
11
|
+
use serde::{Deserialize, Serialize};
|
|
12
|
+
|
|
13
|
+
use crate::emit::{MAX_DISTANCE, MAX_PAYLOAD, Spatial};
|
|
14
|
+
use crate::node::ChunkPos;
|
|
15
|
+
use crate::{Ctx, Error, Result, encode};
|
|
16
|
+
|
|
17
|
+
/// A mob or a trap hit a player.
|
|
18
|
+
pub const CONTACT_DAMAGE: u16 = 77;
|
|
19
|
+
/// The weather or the time of day turned.
|
|
20
|
+
pub const WEATHER: u16 = 90;
|
|
21
|
+
/// A turn passed to the next player.
|
|
22
|
+
pub const TURN: u16 = 91;
|
|
23
|
+
/// Scores or a result.
|
|
24
|
+
pub const SCORE: u16 = 92;
|
|
25
|
+
/// A match was proposed to the players in it.
|
|
26
|
+
pub const PROPOSAL: u16 = 93;
|
|
27
|
+
/// An ability was cast, or landed.
|
|
28
|
+
pub const ABILITY: u16 = 94;
|
|
29
|
+
/// A movement the warden flagged.
|
|
30
|
+
pub const MOVEMENT_VIOLATION: u16 = 95;
|
|
31
|
+
/// A control point changed hands, or its capture moved.
|
|
32
|
+
pub const CONTROL_POINT: u16 = 96;
|
|
33
|
+
/// A racer started, passed a checkpoint, finished a lap or the race.
|
|
34
|
+
pub const RACE_TIMING: u16 = 97;
|
|
35
|
+
/// A play zone moved or shrank.
|
|
36
|
+
pub const ZONE_CHANGE: u16 = 98;
|
|
37
|
+
/// A wave began or ended, or a boss changed phase.
|
|
38
|
+
pub const ENCOUNTER: u16 = 99;
|
|
39
|
+
/// The contested object (a ball) changed hands, or a goal was scored.
|
|
40
|
+
pub const POSSESSION: u16 = 100;
|
|
41
|
+
/// An emote: a client event players send each other (the kit clients' convention); a hub may
|
|
42
|
+
/// send one too, for a server-driven actor.
|
|
43
|
+
pub const EMOTE: u16 = 101;
|
|
44
|
+
/// The first number an app's own events may use.
|
|
45
|
+
pub const APP_EVENTS: u16 = 1000;
|
|
46
|
+
|
|
47
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
48
|
+
#[serde(rename_all = "camelCase")]
|
|
49
|
+
pub struct ContactDamage {
|
|
50
|
+
pub target_uuid: String,
|
|
51
|
+
pub damage: i64,
|
|
52
|
+
pub mob_id: String,
|
|
53
|
+
pub mob_name: String,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
57
|
+
#[serde(rename_all = "camelCase")]
|
|
58
|
+
pub struct WeatherChange {
|
|
59
|
+
pub weather: String,
|
|
60
|
+
pub since_ms: u64,
|
|
61
|
+
pub until_ms: u64,
|
|
62
|
+
pub night: bool,
|
|
63
|
+
pub time_of_day: f64,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
67
|
+
#[serde(rename_all = "camelCase")]
|
|
68
|
+
pub struct TurnChange {
|
|
69
|
+
/// Whose turn it is now: a player id, or a unit's id.
|
|
70
|
+
pub actor_id: String,
|
|
71
|
+
pub round: u32,
|
|
72
|
+
pub turn_in_round: u32,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
76
|
+
#[serde(rename_all = "camelCase")]
|
|
77
|
+
pub struct Standing {
|
|
78
|
+
pub actor_id: String,
|
|
79
|
+
pub score: i64,
|
|
80
|
+
pub rank: u32,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
84
|
+
#[serde(rename_all = "camelCase")]
|
|
85
|
+
pub struct ScoreUpdate {
|
|
86
|
+
pub standings: Vec<Standing>,
|
|
87
|
+
pub winner_id: Option<String>,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
91
|
+
#[serde(rename_all = "camelCase")]
|
|
92
|
+
pub struct ProposalMade {
|
|
93
|
+
pub proposal_id: String,
|
|
94
|
+
pub mode: String,
|
|
95
|
+
pub players: Vec<String>,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
99
|
+
#[serde(rename_all = "camelCase")]
|
|
100
|
+
pub struct AbilityUse {
|
|
101
|
+
/// `cast` or `impact`.
|
|
102
|
+
pub kind: String,
|
|
103
|
+
pub ability_id: String,
|
|
104
|
+
pub caster_id: String,
|
|
105
|
+
pub victim_id: Option<String>,
|
|
106
|
+
pub damage: i64,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
110
|
+
#[serde(rename_all = "camelCase")]
|
|
111
|
+
pub struct MovementViolation {
|
|
112
|
+
/// `speed`, `teleport` or `bounds`.
|
|
113
|
+
pub kind: String,
|
|
114
|
+
pub user_id: String,
|
|
115
|
+
pub detail: String,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
119
|
+
#[serde(rename_all = "camelCase")]
|
|
120
|
+
pub struct ControlPointChange {
|
|
121
|
+
pub point_id: String,
|
|
122
|
+
pub owner: String,
|
|
123
|
+
pub previous_owner: String,
|
|
124
|
+
/// Capture progress toward the attacker, 0 to 1.
|
|
125
|
+
pub progress: f64,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
129
|
+
#[serde(rename_all = "camelCase")]
|
|
130
|
+
pub struct RaceTiming {
|
|
131
|
+
/// `started`, `checkpoint`, `lap` or `finished`.
|
|
132
|
+
pub kind: String,
|
|
133
|
+
pub course_id: String,
|
|
134
|
+
pub user_id: String,
|
|
135
|
+
pub checkpoint: u32,
|
|
136
|
+
pub lap: u32,
|
|
137
|
+
pub elapsed_ms: u64,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
141
|
+
#[serde(rename_all = "camelCase")]
|
|
142
|
+
pub struct ZoneChange {
|
|
143
|
+
/// `hold`, `shrink` or `closed`.
|
|
144
|
+
pub kind: String,
|
|
145
|
+
pub phase: Option<u32>,
|
|
146
|
+
pub radius_now: f64,
|
|
147
|
+
pub center_x: f64,
|
|
148
|
+
pub center_z: f64,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
152
|
+
#[serde(rename_all = "camelCase")]
|
|
153
|
+
pub struct EncounterChange {
|
|
154
|
+
/// `wave_started`, `wave_cleared`, `boss_phase` or `finished`.
|
|
155
|
+
pub kind: String,
|
|
156
|
+
pub encounter_id: String,
|
|
157
|
+
pub wave: u32,
|
|
158
|
+
pub phase: u32,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
162
|
+
#[serde(rename_all = "camelCase")]
|
|
163
|
+
pub struct PossessionChange {
|
|
164
|
+
/// `claimed`, `passed`, `loose` or `goal`.
|
|
165
|
+
pub kind: String,
|
|
166
|
+
pub holder_id: Option<String>,
|
|
167
|
+
pub team: Option<String>,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
171
|
+
#[serde(rename_all = "camelCase")]
|
|
172
|
+
pub struct Emote {
|
|
173
|
+
/// The emote's name, as the game knows it: `wave`, `cheer`, ...
|
|
174
|
+
pub emote: String,
|
|
175
|
+
/// The actor it is aimed at, if any.
|
|
176
|
+
pub target_uuid: Option<String>,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// A server event's payload: its type, then its body as JSON.
|
|
180
|
+
pub fn event_bytes<T: Serialize + ?Sized>(event_type: u16, body: &T) -> Result<Vec<u8>> {
|
|
181
|
+
let json = serde_json::to_vec(body).map_err(|e| Error::new(format!("an event body encodes as JSON: {e}")))?;
|
|
182
|
+
let mut out = Vec::with_capacity(2 + json.len());
|
|
183
|
+
out.extend_from_slice(&event_type.to_le_bytes());
|
|
184
|
+
out.extend_from_slice(&json);
|
|
185
|
+
if out.len() > MAX_PAYLOAD {
|
|
186
|
+
return Err(Error::new(format!("a realtime payload is at most {MAX_PAYLOAD} bytes")));
|
|
187
|
+
}
|
|
188
|
+
Ok(out)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Sends a server event to the players within `distance` chunks of `at` (at most
|
|
192
|
+
/// [`MAX_DISTANCE`]). `actor` is the event's actor: the one it is about, or the hub's own.
|
|
193
|
+
pub fn emit_event<T: Serialize + ?Sized>(ctx: &Ctx, at: ChunkPos, distance: u8, actor: &[u8; 32], event_type: u16, body: &T) -> Result<()> {
|
|
194
|
+
let bytes = event_bytes(event_type, body)?;
|
|
195
|
+
ctx.emit_spatial(at, distance.min(MAX_DISTANCE), actor, Spatial::Event { event_type, state: &bytes[2..] })?;
|
|
196
|
+
Ok(())
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// A server-driven actor as players near it see it.
|
|
200
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
201
|
+
pub struct ActorState {
|
|
202
|
+
/// What it is: `mob`, `npc`, `pet`, `ball`, `ghost`, a game's own word.
|
|
203
|
+
pub kind: String,
|
|
204
|
+
/// Its id within the hub that shows it.
|
|
205
|
+
pub id: u64,
|
|
206
|
+
/// Where it is, in chunks.
|
|
207
|
+
pub x: f64,
|
|
208
|
+
#[serde(default)]
|
|
209
|
+
pub y: f64,
|
|
210
|
+
pub z: f64,
|
|
211
|
+
/// Its health, when it has any.
|
|
212
|
+
pub hp: Option<i64>,
|
|
213
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
214
|
+
pub max_hp: Option<i64>,
|
|
215
|
+
/// What it is doing: `idle`, `chase`, `follow`, ...
|
|
216
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
217
|
+
pub mode: Option<String>,
|
|
218
|
+
/// The player it belongs to (a pet, a summon).
|
|
219
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
220
|
+
pub owner: Option<u64>,
|
|
221
|
+
/// Facing, in radians.
|
|
222
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
223
|
+
pub yaw: Option<f64>,
|
|
224
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
225
|
+
pub name: Option<String>,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
impl ActorState {
|
|
229
|
+
pub fn new(kind: &str, id: u64, x: f64, z: f64) -> Self {
|
|
230
|
+
Self { kind: kind.to_owned(), id, x, y: 0.0, z, hp: None, max_hp: None, mode: None, owner: None, yaw: None, name: None }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// The chunk it stands in.
|
|
234
|
+
pub fn chunk(&self) -> ChunkPos {
|
|
235
|
+
ChunkPos::new(self.x.floor() as i64, self.y.floor() as i64, self.z.floor() as i64)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Shows a server-driven actor to the players within `distance` chunks of where it stands.
|
|
240
|
+
pub fn emit_actor_state(ctx: &Ctx, actor: &[u8; 32], distance: u8, state: &ActorState) -> Result<()> {
|
|
241
|
+
ctx.emit_actor(actor, state.chunk(), distance.min(MAX_DISTANCE), &encode(state)?)?;
|
|
242
|
+
Ok(())
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
#[cfg(test)]
|
|
246
|
+
mod tests {
|
|
247
|
+
use super::*;
|
|
248
|
+
|
|
249
|
+
#[test]
|
|
250
|
+
fn an_event_is_its_type_then_json() {
|
|
251
|
+
let bytes = event_bytes(TURN, &TurnChange { actor_id: "7".into(), round: 2, turn_in_round: 1 }).unwrap();
|
|
252
|
+
assert_eq!(&bytes[..2], &[91, 0]);
|
|
253
|
+
let body: serde_json::Value = serde_json::from_slice(&bytes[2..]).unwrap();
|
|
254
|
+
assert_eq!(body, serde_json::json!({ "actorId": "7", "round": 2, "turnInRound": 1 }));
|
|
255
|
+
assert!(event_bytes(APP_EVENTS, &"x".repeat(MAX_PAYLOAD)).is_err());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[test]
|
|
259
|
+
fn an_actor_state_leaves_out_what_it_does_not_have() {
|
|
260
|
+
let mut s = ActorState::new("mob", 3, 1.5, -2.25);
|
|
261
|
+
s.hp = Some(40);
|
|
262
|
+
let back: serde_json::Value = crate::decode(&encode(&s).unwrap()).unwrap();
|
|
263
|
+
assert_eq!(back, serde_json::json!({ "kind": "mob", "id": 3, "x": 1.5, "y": 0.0, "z": -2.25, "hp": 40 }));
|
|
264
|
+
assert_eq!(s.chunk(), ChunkPos::new(1, 0, -3));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -44,10 +44,15 @@
|
|
|
44
44
|
//! Nodes are unit tested natively with `cargo test`: `testing` runs one against a fake of the
|
|
45
45
|
//! platform (calls, timers, events, snapshots, the node API, a mod's grid). [`limits`] has the
|
|
46
46
|
//! platform's bounds.
|
|
47
|
+
//!
|
|
48
|
+
//! [`kit`] is the game kit's half here: the protocol kit hubs speak to the kit clients
|
|
49
|
+
//! (snapshots and deltas with sequence numbers, retry-safe actions, refusal codes), shard keys,
|
|
50
|
+
//! a seeded random generator, and the realtime event registry.
|
|
47
51
|
|
|
48
52
|
mod abi;
|
|
49
53
|
pub mod cron;
|
|
50
54
|
pub mod emit;
|
|
55
|
+
pub mod kit;
|
|
51
56
|
pub mod limits;
|
|
52
57
|
pub mod model;
|
|
53
58
|
pub mod node;
|
|
@@ -546,20 +551,25 @@ macro_rules! export_spoke {
|
|
|
546
551
|
};
|
|
547
552
|
}
|
|
548
553
|
|
|
554
|
+
// Only the module the sandbox loads needs the exports: a native build (a unit test, a crate
|
|
555
|
+
// that includes several nodes' sources) defines none, so nodes link side by side.
|
|
549
556
|
#[doc(hidden)]
|
|
550
557
|
#[macro_export]
|
|
551
558
|
macro_rules! __export_node {
|
|
552
559
|
($node:ty) => {
|
|
560
|
+
#[cfg(target_arch = "wasm32")]
|
|
553
561
|
#[unsafe(no_mangle)]
|
|
554
562
|
pub extern "C" fn ckx_abi() -> u32 {
|
|
555
563
|
$crate::rt::ABI_VERSION
|
|
556
564
|
}
|
|
557
565
|
|
|
566
|
+
#[cfg(target_arch = "wasm32")]
|
|
558
567
|
#[unsafe(no_mangle)]
|
|
559
568
|
pub extern "C" fn ckx_alloc(len: u32) -> u32 {
|
|
560
569
|
$crate::rt::alloc(len)
|
|
561
570
|
}
|
|
562
571
|
|
|
572
|
+
#[cfg(target_arch = "wasm32")]
|
|
563
573
|
#[unsafe(no_mangle)]
|
|
564
574
|
pub extern "C" fn ckx_init(
|
|
565
575
|
ctx_ptr: u32,
|
|
@@ -574,16 +584,19 @@ macro_rules! __export_node {
|
|
|
574
584
|
$crate::rt::init::<$node>(ctx_ptr, ctx_len, seed_ptr, seed_len, snap_ptr, snap_len, snap_version, has_snap)
|
|
575
585
|
}
|
|
576
586
|
|
|
587
|
+
#[cfg(target_arch = "wasm32")]
|
|
577
588
|
#[unsafe(no_mangle)]
|
|
578
589
|
pub extern "C" fn ckx_handle(caller_kind: u32, caller_id: u64, buf_ptr: u32, method_len: u32, payload_len: u32) -> u64 {
|
|
579
590
|
$crate::rt::handle(caller_kind, caller_id, buf_ptr, method_len, payload_len)
|
|
580
591
|
}
|
|
581
592
|
|
|
593
|
+
#[cfg(target_arch = "wasm32")]
|
|
582
594
|
#[unsafe(no_mangle)]
|
|
583
595
|
pub extern "C" fn ckx_persist() -> u64 {
|
|
584
596
|
$crate::rt::persist()
|
|
585
597
|
}
|
|
586
598
|
|
|
599
|
+
#[cfg(target_arch = "wasm32")]
|
|
587
600
|
#[unsafe(no_mangle)]
|
|
588
601
|
pub extern "C" fn ckx_evict() {
|
|
589
602
|
$crate::rt::evict()
|
|
@@ -209,6 +209,11 @@ impl Model {
|
|
|
209
209
|
self.containers.get(&id)
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/// Every container's id, oldest first.
|
|
213
|
+
pub fn ids(&self) -> impl Iterator<Item = u64> + '_ {
|
|
214
|
+
self.containers.keys().copied()
|
|
215
|
+
}
|
|
216
|
+
|
|
212
217
|
/// Every container of `type_name`, oldest first.
|
|
213
218
|
pub fn of_type<'a>(&'a self, type_name: &'a str) -> impl Iterator<Item = u64> + 'a {
|
|
214
219
|
self.containers.iter().filter(move |(_, c)| c.type_name == type_name).map(|(id, _)| *id)
|
package/dist/cli/src/new.js
CHANGED
|
@@ -15,6 +15,15 @@ exports.TEMPLATES = {
|
|
|
15
15
|
matchmaker: { dir: ['starters', 'matchmaker'], sdk: 'ckx-sdk', about: 'a rating queue that reserves sessions' },
|
|
16
16
|
session: { dir: ['starters', 'session'], sdk: 'ckx-sdk', about: 'one game from lobby to result' },
|
|
17
17
|
'npc-mobs': { dir: ['starters', 'npc-mobs'], sdk: 'ckx-sdk', about: "an area's mobs and NPCs" },
|
|
18
|
+
leaderboard: { dir: ['starters', 'leaderboard'], sdk: 'ckx-sdk', about: 'a board other hubs submit to, with seasons' },
|
|
19
|
+
deck: { dir: ['starters', 'deck'], sdk: 'ckx-sdk', about: 'a card table with hands only their player reads' },
|
|
20
|
+
tactics: { dir: ['starters', 'tactics'], sdk: 'ckx-sdk', about: "a grid battle in phases against the hub's units" },
|
|
21
|
+
director: { dir: ['starters', 'director'], sdk: 'ckx-sdk', about: 'waves down a lane, towers, a boss with phases' },
|
|
22
|
+
minigame: { dir: ['starters', 'minigame'], sdk: 'ckx-sdk', about: 'a duel of hidden throws, revealed together' },
|
|
23
|
+
instance: { dir: ['starters', 'instance'], sdk: 'ckx-sdk', about: "a party's private run: seeded floors, permadeath" },
|
|
24
|
+
arena: { dir: ['starters', 'arena'], sdk: 'ckx-sdk', about: 'classes and abilities in real time' },
|
|
25
|
+
racing: { dir: ['starters', 'racing'], sdk: 'ckx-sdk', about: 'races with laps and a ghost, and ball games' },
|
|
26
|
+
territory: { dir: ['starters', 'territory'], sdk: 'ckx-sdk', about: 'control points, and a shrinking circle' },
|
|
18
27
|
};
|
|
19
28
|
const CRATE_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
20
29
|
function copyTree(from, to) {
|
package/kit.pin
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# The platform toolchain release this kit was copied from (sync-kit.sh in the platform's
|
|
2
2
|
# repository). The paths in `copied` are byte-identical to it: change them there, never here.
|
|
3
|
-
toolchain_version=v1.
|
|
4
|
-
toolchain_commit=
|
|
5
|
-
sdk_commit=
|
|
6
|
-
ckx_sdk_version=0.
|
|
3
|
+
toolchain_version=v1.13.0
|
|
4
|
+
toolchain_commit=4e6f1a7c9502d5d7171d75b6063acec97172239e
|
|
5
|
+
sdk_commit=04d42bc77e24b6d84debc71eeee0efc687c0ee9a
|
|
6
|
+
ckx_sdk_version=0.11.0
|
|
7
7
|
crowdy_client_sdk_version=0.1.0
|
|
8
8
|
rust_version=1.97.1
|
|
9
9
|
copied=crates/ckx-sdk/src crates/crowdy-client-sdk/src tools/instrument/Cargo.toml tools/instrument/Cargo.lock tools/instrument/src checks/src/build/exec-source.ts checks/src/build/exec-source.spec.ts checks/src/build/rust-source-guard.ts checks/src/build/rust-source-guard.spec.ts checks/src/build/wasm-inspect.ts checks/src/build/wasm-inspect.spec.ts checks/src/build/client-capabilities.ts checks/src/build/client-capabilities.spec.ts checks/src/test-support/wasm-module.ts checks/compute-toolchain/host-catalog.json starters rust-toolchain.toml vendor.lock
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crowdedkingdoms/ckx-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-dev.1",
|
|
4
4
|
"description": "Build, check and test Crowded Kingdoms ck-exec mods, hubs and spokes on your machine, as the platform builds them",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"homepage": "https://docs.crowdedkingdoms.com/exec/intro",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "arena"
|
|
3
|
+
description = "Starter: a real-time arena where the hub referees every hit: classes with abilities (hit-scan, projectiles, area damage, statuses), respawns, kill credit, and a warden on the positions clients report"
|
|
4
|
+
version = "0.1.0"
|
|
5
|
+
edition = "2024"
|
|
6
|
+
publish = false
|
|
7
|
+
|
|
8
|
+
[lib]
|
|
9
|
+
crate-type = ["cdylib"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
ckx-sdk = "0.11.0"
|
|
13
|
+
serde = { version = "1", features = ["derive"] }
|
|
14
|
+
serde_json = "1"
|