steel_core/player/
experience.rs1use steel_protocol::packets::game::SoundSource;
2use steel_registry::sound_events;
3
4use crate::entity::Entity;
5
6use super::Player;
7
8#[derive(Default, Copy, Clone, Debug)]
14pub struct Experience {
15 level: i32,
16 progress: f32,
17 total_points: i32,
18 enchantment_seed: i32,
19 pub dirty: bool,
21}
22
23impl Experience {
24 #[must_use]
26 pub fn new(total_points: i32) -> Self {
27 let total_points = total_points.max(0);
28 let level = level_for_total_points(total_points);
29 let points_into_level = total_points - Self::total_points_at_level(level);
30 let points_for_level = Self::points_for_level(level);
31 let progress = if points_for_level > 0 {
32 points_into_level as f32 / points_for_level as f32
33 } else {
34 0.0
35 };
36
37 Self {
38 level,
39 progress,
40 total_points,
41 enchantment_seed: 0,
42 dirty: true,
43 }
44 }
45
46 #[must_use]
48 pub const fn from_parts(level: i32, progress: f32, total_points: i32) -> Self {
49 Self {
50 level,
51 progress,
52 total_points,
53 enchantment_seed: 0,
54 dirty: true,
55 }
56 }
57
58 #[must_use]
60 pub const fn points_for_level(level: i32) -> i32 {
61 if level >= 30 {
62 level.wrapping_sub(30).wrapping_mul(9).wrapping_add(112)
63 } else if level >= 15 {
64 level.wrapping_sub(15).wrapping_mul(5).wrapping_add(37)
65 } else {
66 level.wrapping_mul(2).wrapping_add(7)
67 }
68 }
69
70 #[must_use]
74 pub const fn total_points_at_level(level: i32) -> i32 {
75 if level <= 0 {
76 return 0;
77 }
78
79 let level = level as i128;
80 let points = if level <= 15 {
81 level * level + 6 * level
82 } else if level <= 30 {
83 360 + level * (5 * level - 81) / 2
84 } else {
85 level * (9 * level - 325) / 2 + 2220
86 };
87 if points > i32::MAX as i128 {
88 i32::MAX
89 } else {
90 points as i32
91 }
92 }
93
94 #[must_use]
96 pub const fn level(&self) -> i32 {
97 self.level
98 }
99
100 #[must_use]
102 pub fn points(&self) -> i32 {
103 (self.progress * Self::points_for_level(self.level) as f32).floor() as i32
104 }
105
106 #[must_use]
108 pub const fn total_points(&self) -> i32 {
109 self.total_points
110 }
111
112 #[must_use]
114 pub const fn progress(&self) -> f32 {
115 self.progress
116 }
117
118 #[must_use]
120 pub const fn enchantment_seed(&self) -> i32 {
121 self.enchantment_seed
122 }
123
124 #[must_use]
126 pub const fn with_enchantment_seed(mut self, seed: i32) -> Self {
127 self.enchantment_seed = seed;
128 self
129 }
130
131 #[must_use]
133 pub fn with_loaded_enchantment_seed(self, saved: i32, reroll: impl FnOnce() -> i32) -> Self {
134 let seed = if saved == 0 { reroll() } else { saved };
135 self.with_enchantment_seed(seed)
136 }
137
138 pub const fn add_levels(&mut self, additional_levels: i32) {
140 if additional_levels == 0 {
141 return;
142 }
143
144 self.level = self.level.saturating_add(additional_levels);
145 if self.level < 0 {
146 self.level = 0;
147 self.progress = 0.0;
148 self.total_points = 0;
149 }
150 self.dirty = true;
151 }
152
153 pub const fn on_enchantment_performed(&mut self, enchantment_cost: i32, next_seed: i32) {
155 self.level = self.level.wrapping_sub(enchantment_cost);
156 if self.level < 0 {
157 self.level = 0;
158 self.progress = 0.0;
159 self.total_points = 0;
160 }
161 self.enchantment_seed = next_seed;
162 self.dirty = true;
163 }
164
165 #[expect(
167 clippy::cast_precision_loss,
168 reason = "vanilla performs these calculations with Java float precision"
169 )]
170 pub fn add_points(&mut self, additional_points: i32) {
171 if additional_points == 0 {
172 return;
173 }
174
175 self.progress += additional_points as f32 / Self::points_for_level(self.level) as f32;
176 self.total_points = self.total_points.wrapping_add(additional_points).max(0);
177
178 while self.progress < 0.0 {
179 let remaining = self.progress * Self::points_for_level(self.level) as f32;
180 if self.level > 0 {
181 self.add_levels(-1);
182 self.progress = 1.0 + remaining / Self::points_for_level(self.level) as f32;
183 } else {
184 self.add_levels(-1);
185 self.progress = 0.0;
186 }
187 }
188
189 while self.progress >= 1.0 {
190 self.progress = (self.progress - 1.0) * Self::points_for_level(self.level) as f32;
191 self.add_levels(1);
192 self.progress /= Self::points_for_level(self.level) as f32;
193 }
194
195 self.dirty = true;
196 }
197
198 pub const fn set_levels(&mut self, level: i32) {
200 if self.level != level {
201 self.level = level;
202 self.dirty = true;
203 }
204 }
205
206 #[must_use]
208 pub const fn can_set_points(&self, points: i32) -> bool {
209 points >= 0 && points < Self::points_for_level(self.level)
210 }
211
212 #[expect(
214 clippy::cast_precision_loss,
215 reason = "vanilla performs these calculations with Java float precision"
216 )]
217 pub fn set_points(&mut self, points: i32) {
218 let limit = Self::points_for_level(self.level) as f32;
219 let maximum = (limit - 1.0) / limit;
220 let requested = points as f32 / limit;
221 let progress = if requested < 0.0 {
222 0.0
223 } else {
224 requested.min(maximum)
225 };
226 if self.progress.to_bits() != progress.to_bits() {
227 self.progress = progress;
228 self.dirty = true;
229 }
230 }
231
232 pub const fn clear(&mut self) {
234 self.level = 0;
235 self.progress = 0.0;
236 self.total_points = 0;
237 self.dirty = true;
238 }
239
240 #[must_use]
242 pub const fn death_xp_reward(&self) -> i32 {
243 let reward = self.level.wrapping_mul(7);
244 if reward < 100 { reward } else { 100 }
245 }
246}
247
248fn level_for_total_points(total_points: i32) -> i32 {
249 let points = f64::from(total_points);
250 if points <= 315.0 {
251 return f64::midpoint(-6.0, f64::sqrt(36.0 + 4.0 * points)) as i32;
252 }
253 if points <= 1507.0 {
254 return ((40.5 + f64::sqrt(-1959.75 + 10.0 * points)) / 5.0) as i32;
255 }
256 ((162.5 + f64::sqrt(-13553.75 + 18.0 * points)) / 9.0) as i32
257}
258
259impl Player {
260 #[must_use]
262 pub fn score(&self) -> i32 {
263 *self.entity_data.lock().score.get()
264 }
265
266 pub fn set_score(&self, score: i32) {
268 self.entity_data.lock().score.set(score);
269 }
270
271 fn increase_score(&self, amount: i32) {
272 let mut entity_data = self.entity_data.lock();
273 let score = entity_data.score.get().wrapping_add(amount);
274 entity_data.score.set(score);
275 }
276
277 pub(crate) fn give_experience_points(&self, points: i32) {
279 if points == 0 {
280 return;
281 }
282 self.increase_score(points);
283 let level_up_sound = {
284 let mut experience = self.experience.lock();
285 let old_level = experience.level();
286 experience.add_points(points);
287 first_point_level_up_sound(old_level, experience.level(), points)
288 };
289 if let Some(level) = level_up_sound {
290 self.play_experience_level_up_sound(level);
291 }
292 }
293
294 pub(crate) fn give_experience_levels(&self, levels: i32) {
296 let level_up_sound = {
297 let mut experience = self.experience.lock();
298 experience.add_levels(levels);
299 (levels > 0 && experience.level() % 5 == 0).then_some(experience.level())
300 };
301 if let Some(level) = level_up_sound {
302 self.play_experience_level_up_sound(level);
303 }
304 }
305
306 fn play_experience_level_up_sound(&self, level: i32) {
307 if !self.tick_state.lock().mark_level_up_sound_if_due() {
308 return;
309 }
310 let volume = if level > 30 { 1.0 } else { level as f32 / 30.0 };
311 self.get_world().play_sound_at(
313 &sound_events::ENTITY_PLAYER_LEVELUP,
314 SoundSource::Players,
315 self.position(),
316 volume * 0.75,
317 1.0,
318 None,
319 );
320 }
321
322 pub(super) fn advance_tick(&self) {
324 self.tick_state.lock().advance_tick();
325 }
326}
327
328pub(super) fn first_point_level_up_sound(
329 old_level: i32,
330 new_level: i32,
331 points: i32,
332) -> Option<i32> {
333 if points <= 0 || new_level <= old_level {
334 return None;
335 }
336 let first_multiple = (i64::from(old_level).div_euclid(5) + 1) * 5;
337 if first_multiple > i64::from(new_level) {
338 return None;
339 }
340 i32::try_from(first_multiple).ok()
341}