Skip to content

Commit

Permalink
feat: Add player experience (XP) system and syncing
Browse files Browse the repository at this point in the history
Implements XP tracking and visualization with the following changes:
- Add `Xp` component to track player experience points
- Add `XpVisual` helper to calculate level/progress from XP amount
- Add system to sync XP changes to clients
- Update `/xp` command to modify player's XP directly
- Initialize XP component during player login

The implementation follows Minecraft's XP/level calculation formula,
supporting levels 0-63 with proper progress bar visualization.
  • Loading branch information
andrewgazelka committed Nov 13, 2024
1 parent a7c168e commit 50c1f10
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 21 deletions.
36 changes: 34 additions & 2 deletions crates/hyperion/src/egress/sync_entity_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ use crate::{
egress::metadata::show_all,
net::{Compose, NetworkStreamRef, agnostic},
simulation::{
EntityReaction, Health, Pitch, Position, Yaw,
EntityReaction, Health, Pitch, Position, Xp, Yaw,
animation::ActiveAnimation,
metadata::{EntityFlags, MetadataBuilder, Pose},
},
storage::ThreadLocal,
system_registry::SYNC_ENTITY_POSITION,
system_registry::{SYNC_ENTITY_POSITION, SystemId},
util::TracingExt,
};

Expand All @@ -36,6 +36,38 @@ impl Module for EntityStateSyncModule {

let metadata: ThreadLocal<UnsafeCell<MetadataBuilder>> = ThreadLocal::new_defaults();

system!(
"entity_xp_sync",
world,
&Compose($),
&NetworkStreamRef,
&mut Prev<Xp>,
&mut Xp,
)
.multi_threaded()
.kind::<flecs::pipeline::OnStore>()
.tracing_each_entity(
info_span!("entity_xp_sync"),
move |entity, (compose, net, Prev(prev_xp), xp)| {
let world = entity.world();
if prev_xp.amount != xp.amount {
let visual = xp.get_visual();

let packet = play::ExperienceBarUpdateS2c {
bar: visual.prop,
level: VarInt(i32::from(visual.level)),
total_xp: VarInt::default(),
};

compose
.unicast(&packet, *net, SystemId(100), &world)
.unwrap();

*prev_xp = *xp;
}
},
);

system!(
"entity_state_sync",
world,
Expand Down
4 changes: 3 additions & 1 deletion crates/hyperion/src/ingress/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::{
simulation::{
AiTargetable, ChunkPosition, Comms, ConfirmBlockSequences, EntityReaction, EntitySize,
Health, IgnMap, ImmuneStatus, InGameName, PacketState, Pitch, Player, Position,
StreamLookup, Uuid, Yaw,
StreamLookup, Uuid, Xp, Yaw,
animation::ActiveAnimation,
blocks::Blocks,
handlers::PacketSwitchQuery,
Expand Down Expand Up @@ -170,6 +170,8 @@ fn process_login(
.set(Uuid::from(uuid))
.set(Prev(Health::default()))
.add::<Health>()
.set(Prev(Xp::default()))
.add::<Xp>()
.set(Prev(EntityFlags::default()))
.set(EntityFlags::default())
.set(Prev(Pose::default()))
Expand Down
159 changes: 159 additions & 0 deletions crates/hyperion/src/simulation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,162 @@ pub enum PacketState {
#[meta]
pub struct Health(f32);

#[derive(
Component, Debug, Deref, DerefMut, PartialEq, Eq, PartialOrd, Copy, Clone, Default
)]
#[meta]
pub struct Xp {
pub amount: u16,
}

pub struct XpVisual {
pub level: u8,
pub prop: f32,
}

impl Xp {
#[must_use]
pub fn get_visual(&self) -> XpVisual {
let level = match self.amount {
0..=6 => 0,
7..=15 => 1,
16..=26 => 2,
27..=39 => 3,
40..=54 => 4,
55..=71 => 5,
72..=90 => 6,
91..=111 => 7,
112..=134 => 8,
135..=159 => 9,
160..=186 => 10,
187..=215 => 11,
216..=246 => 12,
247..=279 => 13,
280..=314 => 14,
315..=351 => 15,
352..=393 => 16,
394..=440 => 17,
441..=492 => 18,
493..=549 => 19,
550..=611 => 20,
612..=678 => 21,
679..=750 => 22,
751..=827 => 23,
828..=909 => 24,
910..=996 => 25,
997..=1088 => 26,
1089..=1185 => 27,
1186..=1287 => 28,
1288..=1394 => 29,
1395..=1506 => 30,
1507..=1627 => 31,
1628..=1757 => 32,
1758..=1896 => 33,
1897..=2044 => 34,
2045..=2201 => 35,
2202..=2367 => 36,
2368..=2542 => 37,
2543..=2726 => 38,
2727..=2919 => 39,
2920..=3121 => 40,
3122..=3332 => 41,
3333..=3552 => 42,
3553..=3781 => 43,
3782..=4019 => 44,
4020..=4266 => 45,
4267..=4522 => 46,
4523..=4787 => 47,
4788..=5061 => 48,
5062..=5344 => 49,
5345..=5636 => 50,
5637..=5937 => 51,
5938..=6247 => 52,
6248..=6566 => 53,
6567..=6894 => 54,
6895..=7231 => 55,
7232..=7577 => 56,
7578..=7932 => 57,
7933..=8296 => 58,
8297..=8669 => 59,
8670..=9051 => 60,
9052..=9442 => 61,
9443..=9842 => 62,
_ => 63,
};

let (level_start, next_level_start) = match level {
0 => (0, 7),
1 => (7, 16),
2 => (16, 27),
3 => (27, 40),
4 => (40, 55),
5 => (55, 72),
6 => (72, 91),
7 => (91, 112),
8 => (112, 135),
9 => (135, 160),
10 => (160, 187),
11 => (187, 216),
12 => (216, 247),
13 => (247, 280),
14 => (280, 315),
15 => (315, 352),
16 => (352, 394),
17 => (394, 441),
18 => (441, 493),
19 => (493, 550),
20 => (550, 612),
21 => (612, 679),
22 => (679, 751),
23 => (751, 828),
24 => (828, 910),
25 => (910, 997),
26 => (997, 1089),
27 => (1089, 1186),
28 => (1186, 1288),
29 => (1288, 1395),
30 => (1395, 1507),
31 => (1507, 1628),
32 => (1628, 1758),
33 => (1758, 1897),
34 => (1897, 2045),
35 => (2045, 2202),
36 => (2202, 2368),
37 => (2368, 2543),
38 => (2543, 2727),
39 => (2727, 2920),
40 => (2920, 3122),
41 => (3122, 3333),
42 => (3333, 3553),
43 => (3553, 3782),
44 => (3782, 4020),
45 => (4020, 4267),
46 => (4267, 4523),
47 => (4523, 4788),
48 => (4788, 5062),
49 => (5062, 5345),
50 => (5345, 5637),
51 => (5637, 5938),
52 => (5938, 6248),
53 => (6248, 6567),
54 => (6567, 6895),
55 => (6895, 7232),
56 => (7232, 7578),
57 => (7578, 7933),
58 => (7933, 8297),
59 => (8297, 8670),
60 => (8670, 9052),
61 => (9052, 9443),
62 => (9443, 9843),
_ => (9843, 10242), // Extrapolated next value
};

let prop = (self.amount - level_start) as f32 / (next_level_start - level_start) as f32;

XpVisual { level, prop }
}
}

impl Metadata for Health {
type Type = f32;

Expand Down Expand Up @@ -431,6 +587,9 @@ impl Module for SimModule {
world.component::<Health>().member::<f32>("level");
world.component::<Prev<Health>>();

world.component::<Xp>();
world.component::<Prev<Xp>>();

world.component::<PlayerSkin>();
world.component::<Command>();

Expand Down
23 changes: 5 additions & 18 deletions events/proof-of-concept/src/command/xp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use clap::Parser;
use flecs_ecs::core::{Entity, EntityViewGet, World, WorldGet};
use hyperion::{
net::{Compose, DataBundle, NetworkStreamRef},
simulation::Xp,
system_registry::SystemId,
valence_protocol::{VarInt, packets::play},
};
Expand All @@ -10,29 +11,15 @@ use hyperion_clap::MinecraftCommand;
#[derive(Parser, Debug)]
#[command(name = "xp")]
pub struct XpCommand {
bar: f32,
level: i32,
amount: u16,
}

impl MinecraftCommand for XpCommand {
fn execute(self, world: &World, caller: Entity) {
let Self { bar, level } = self;
let Self { amount } = self;

let xp_pkt = play::ExperienceBarUpdateS2c {
bar,
level: VarInt(level),
total_xp: VarInt::default(),
};

world.get::<&Compose>(|compose| {
caller
.entity_view(world)
.get::<&NetworkStreamRef>(|stream| {
let mut bundle = DataBundle::new(compose);
bundle.add_packet(&xp_pkt, world).unwrap();

bundle.send(world, *stream, SystemId(8)).unwrap();
});
caller.entity_view(world).get::<&mut Xp>(|xp| {
xp.amount = amount;
});
}
}

0 comments on commit 50c1f10

Please sign in to comment.