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 pub dirty: bool,
20}
21
22impl Experience {
23 #[must_use]
25 pub fn new(total_points: i32) -> Self {
26 let total_points = total_points.max(0);
27 let level = level_for_total_points(total_points);
28 let points_into_level = total_points - Self::total_points_at_level(level);
29 let points_for_level = Self::points_for_level(level);
30 let progress = if points_for_level > 0 {
31 points_into_level as f32 / points_for_level as f32
32 } else {
33 0.0
34 };
35
36 Self {
37 level,
38 progress,
39 total_points,
40 dirty: true,
41 }
42 }
43
44 #[must_use]
46 pub const fn from_parts(level: i32, progress: f32, total_points: i32) -> Self {
47 Self {
48 level,
49 progress,
50 total_points,
51 dirty: true,
52 }
53 }
54
55 #[must_use]
57 pub const fn points_for_level(level: i32) -> i32 {
58 if level >= 30 {
59 level.wrapping_sub(30).wrapping_mul(9).wrapping_add(112)
60 } else if level >= 15 {
61 level.wrapping_sub(15).wrapping_mul(5).wrapping_add(37)
62 } else {
63 level.wrapping_mul(2).wrapping_add(7)
64 }
65 }
66
67 #[must_use]
71 pub const fn total_points_at_level(level: i32) -> i32 {
72 if level <= 0 {
73 return 0;
74 }
75
76 let level = level as i128;
77 let points = if level <= 15 {
78 level * level + 6 * level
79 } else if level <= 30 {
80 360 + level * (5 * level - 81) / 2
81 } else {
82 level * (9 * level - 325) / 2 + 2220
83 };
84 if points > i32::MAX as i128 {
85 i32::MAX
86 } else {
87 points as i32
88 }
89 }
90
91 #[must_use]
93 pub const fn level(&self) -> i32 {
94 self.level
95 }
96
97 #[must_use]
99 pub fn points(&self) -> i32 {
100 (self.progress * Self::points_for_level(self.level) as f32).floor() as i32
101 }
102
103 #[must_use]
105 pub const fn total_points(&self) -> i32 {
106 self.total_points
107 }
108
109 #[must_use]
111 pub const fn progress(&self) -> f32 {
112 self.progress
113 }
114
115 pub const fn add_levels(&mut self, additional_levels: i32) {
117 if additional_levels == 0 {
118 return;
119 }
120
121 self.level = self.level.saturating_add(additional_levels);
122 if self.level < 0 {
123 self.level = 0;
124 self.progress = 0.0;
125 self.total_points = 0;
126 }
127 self.dirty = true;
128 }
129
130 #[expect(
132 clippy::cast_precision_loss,
133 reason = "vanilla performs these calculations with Java float precision"
134 )]
135 pub fn add_points(&mut self, additional_points: i32) {
136 if additional_points == 0 {
137 return;
138 }
139
140 self.progress += additional_points as f32 / Self::points_for_level(self.level) as f32;
141 self.total_points = self.total_points.wrapping_add(additional_points).max(0);
142
143 while self.progress < 0.0 {
144 let remaining = self.progress * Self::points_for_level(self.level) as f32;
145 if self.level > 0 {
146 self.add_levels(-1);
147 self.progress = 1.0 + remaining / Self::points_for_level(self.level) as f32;
148 } else {
149 self.add_levels(-1);
150 self.progress = 0.0;
151 }
152 }
153
154 while self.progress >= 1.0 {
155 self.progress = (self.progress - 1.0) * Self::points_for_level(self.level) as f32;
156 self.add_levels(1);
157 self.progress /= Self::points_for_level(self.level) as f32;
158 }
159
160 self.dirty = true;
161 }
162
163 pub const fn set_levels(&mut self, level: i32) {
165 if self.level != level {
166 self.level = level;
167 self.dirty = true;
168 }
169 }
170
171 #[must_use]
173 pub const fn can_set_points(&self, points: i32) -> bool {
174 points >= 0 && points < Self::points_for_level(self.level)
175 }
176
177 #[expect(
179 clippy::cast_precision_loss,
180 reason = "vanilla performs these calculations with Java float precision"
181 )]
182 pub fn set_points(&mut self, points: i32) {
183 let limit = Self::points_for_level(self.level) as f32;
184 let maximum = (limit - 1.0) / limit;
185 let requested = points as f32 / limit;
186 let progress = if requested < 0.0 {
187 0.0
188 } else {
189 requested.min(maximum)
190 };
191 if self.progress.to_bits() != progress.to_bits() {
192 self.progress = progress;
193 self.dirty = true;
194 }
195 }
196
197 pub const fn clear(&mut self) {
199 self.level = 0;
200 self.progress = 0.0;
201 self.total_points = 0;
202 self.dirty = true;
203 }
204
205 #[must_use]
207 pub const fn death_xp_reward(&self) -> i32 {
208 let reward = self.level.wrapping_mul(7);
209 if reward < 100 { reward } else { 100 }
210 }
211}
212
213fn level_for_total_points(total_points: i32) -> i32 {
214 let points = f64::from(total_points);
215 if points <= 315.0 {
216 return f64::midpoint(-6.0, f64::sqrt(36.0 + 4.0 * points)) as i32;
217 }
218 if points <= 1507.0 {
219 return ((40.5 + f64::sqrt(-1959.75 + 10.0 * points)) / 5.0) as i32;
220 }
221 ((162.5 + f64::sqrt(-13553.75 + 18.0 * points)) / 9.0) as i32
222}
223
224impl Player {
225 #[must_use]
227 pub fn score(&self) -> i32 {
228 *self.entity_data.lock().score.get()
229 }
230
231 pub fn set_score(&self, score: i32) {
233 self.entity_data.lock().score.set(score);
234 }
235
236 fn increase_score(&self, amount: i32) {
237 let mut entity_data = self.entity_data.lock();
238 let score = entity_data.score.get().wrapping_add(amount);
239 entity_data.score.set(score);
240 }
241
242 pub(crate) fn give_experience_points(&self, points: i32) {
244 if points == 0 {
245 return;
246 }
247 self.increase_score(points);
248 let level_up_sound = {
249 let mut experience = self.experience.lock();
250 let old_level = experience.level();
251 experience.add_points(points);
252 first_point_level_up_sound(old_level, experience.level(), points)
253 };
254 if let Some(level) = level_up_sound {
255 self.play_experience_level_up_sound(level);
256 }
257 }
258
259 pub(crate) fn give_experience_levels(&self, levels: i32) {
261 let level_up_sound = {
262 let mut experience = self.experience.lock();
263 experience.add_levels(levels);
264 (levels > 0 && experience.level() % 5 == 0).then_some(experience.level())
265 };
266 if let Some(level) = level_up_sound {
267 self.play_experience_level_up_sound(level);
268 }
269 }
270
271 fn play_experience_level_up_sound(&self, level: i32) {
272 if !self.tick_state.lock().mark_level_up_sound_if_due() {
273 return;
274 }
275 let volume = if level > 30 { 1.0 } else { level as f32 / 30.0 };
276 self.get_world().play_sound_at(
278 &sound_events::ENTITY_PLAYER_LEVELUP,
279 SoundSource::Players,
280 self.position(),
281 volume * 0.75,
282 1.0,
283 None,
284 );
285 }
286
287 pub(super) fn advance_tick(&self) {
289 self.tick_state.lock().advance_tick();
290 }
291}
292
293pub(super) fn first_point_level_up_sound(
294 old_level: i32,
295 new_level: i32,
296 points: i32,
297) -> Option<i32> {
298 if points <= 0 || new_level <= old_level {
299 return None;
300 }
301 let first_multiple = (i64::from(old_level).div_euclid(5) + 1) * 5;
302 if first_multiple > i64::from(new_level) {
303 return None;
304 }
305 i32::try_from(first_multiple).ok()
306}