1use configs::game_config::GameConfig;
13use essences::entity::Entity;
14use essences::fighting::ActiveFight;
15use event_system::script::random::GameRng;
16
17use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
18use crate::event::CustomEventData;
19use crate::event::OverlordEvent;
20use crate::mechanics::content_lookups::ContentLookups;
21use crate::mechanics::fight::{self, NativeSink};
22
23pub struct EventCtx<'a> {
26 pub entity: &'a Entity,
27 pub fight: &'a ActiveFight,
28 pub rng: &'a GameRng,
30 pub current_tick: u64,
31 pub fight_duration_ticks: u64,
32 pub caller_event: Option<&'a OverlordEvent>,
34 pub config: &'a GameConfig,
35 pub lookups: &'a ContentLookups,
36}
37
38pub type EventFn = fn(&EventCtx) -> anyhow::Result<Vec<OverlordEvent>>;
40
41fn attr(entity: &Entity, name: &str) -> Option<i64> {
43 entity.attributes.0.get(name).copied()
44}
45
46fn incr(entity_id: uuid::Uuid, attribute: &str, delta: i64) -> OverlordEvent {
48 OverlordEvent::EntityIncrAttribute {
49 entity_id,
50 attribute: attribute.to_string(),
51 delta,
52 }
53}
54
55fn duration_decrement(ctx: &EventCtx, attr_name: &str, tick: i64) -> Vec<OverlordEvent> {
58 let Some(dur) = attr(ctx.entity, attr_name) else {
59 return vec![];
60 };
61 let delta = if dur > tick { -tick } else { -dur };
62 vec![incr(ctx.entity.id, attr_name, delta)]
63}
64
65pub fn protection_duration_decrement(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
66 Ok(duration_decrement(ctx, "effect.protection.duration", 100))
67}
68
69pub fn empower_duration_decrement(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
70 Ok(duration_decrement(ctx, "effect.empower.duration", 100))
71}
72
73pub fn vulnerability_duration_decrement(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
74 Ok(duration_decrement(
75 ctx,
76 "effect.vulnerability.duration",
77 1000,
78 ))
79}
80
81pub fn weakness_duration_decrement(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
82 Ok(duration_decrement(ctx, "effect.weakness.duration", 100))
83}
84
85pub fn sleep_wake_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
89 let id = ctx.entity.id;
90 let remaining = attr(ctx.entity, "wake_up_delay").unwrap_or(0);
92 let sleep = attr(ctx.entity, "sleep");
93 let mut events = Vec::new();
94 if sleep.is_none() {
95 events.push(incr(id, "wake_up_delay", -remaining));
96 } else {
97 events.push(incr(id, "wake_up_delay", -1));
98 if remaining <= 1 {
99 events.push(incr(id, "sleep", -sleep.unwrap_or(0)));
100 }
101 }
102 Ok(events)
103}
104
105pub fn regeneration_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
109 let mut sink = NativeSink::default();
110 let rate = fight::get_entity_stat(ctx.lookups, ctx.entity, "regeneration_rate");
111 let capped = crate::mechanics::balance::cap_per_tick(
115 rate,
116 ctx.entity.max_hp as f64,
117 crate::mechanics::balance::tuning().regen_tick_max_pct,
118 );
119 fight::heal_entity(&mut sink, ctx.entity, capped)
120 .map_err(|e| anyhow::anyhow!("heal_entity: {e}"))?;
121 Ok(sink.events)
122}
123
124pub fn hot_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
132 let mut sink = NativeSink::default();
133 let next_attr = "effect.hot.next";
134 let next_tick_number = attr(ctx.entity, next_attr).unwrap_or(0);
136 let amount_attr = format!("effect.hot.tick.{next_tick_number}");
137 match attr(ctx.entity, &amount_attr) {
138 None => {
139 fight::set_entity_attr(&mut sink, ctx.entity, next_attr, 0.0)
140 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
141 }
142 Some(next_tick_amount) => {
143 if next_tick_number == 5 {
144 fight::set_entity_attr(&mut sink, ctx.entity, next_attr, 1.0)
145 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
146 } else {
147 fight::add_entity_attr(&mut sink, ctx.entity, next_attr, 1)
148 .map_err(|e| anyhow::anyhow!("add_entity_attr: {e}"))?;
149 }
150 fight::set_entity_attr(&mut sink, ctx.entity, &amount_attr, 0.0)
151 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
152 let capped = crate::mechanics::balance::cap_per_tick(
155 next_tick_amount as f64,
156 ctx.entity.max_hp as f64,
157 crate::mechanics::balance::tuning().hot_tick_max_pct,
158 );
159 fight::heal_entity(&mut sink, ctx.entity, capped)
160 .map_err(|e| anyhow::anyhow!("heal_entity: {e}"))?;
161 }
162 }
163 Ok(sink.events)
164}
165
166pub fn dot_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
172 let mut sink = NativeSink::default();
173 let next_attr = "effect.dot.next";
174 let next_tick_number = attr(ctx.entity, next_attr).unwrap_or(0);
175 let amount_attr = format!("effect.dot.tick.{next_tick_number}");
176 match attr(ctx.entity, &amount_attr) {
177 None => {
178 fight::set_entity_attr(&mut sink, ctx.entity, next_attr, 0.0)
179 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
180 }
181 Some(next_tick_amount) => {
182 if next_tick_number == 5 {
183 fight::set_entity_attr(&mut sink, ctx.entity, next_attr, 1.0)
184 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
185 } else {
186 fight::add_entity_attr(&mut sink, ctx.entity, next_attr, 1)
187 .map_err(|e| anyhow::anyhow!("add_entity_attr: {e}"))?;
188 }
189 let mut custom_data = CustomEventData::default();
190 custom_data.add("dot", 1);
191 custom_data.add("no_hit_anim", 1);
192 fight::set_entity_attr(&mut sink, ctx.entity, &amount_attr, 0.0)
193 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
194 let capped = crate::mechanics::balance::cap_per_tick(
198 next_tick_amount as f64,
199 ctx.entity.max_hp as f64,
200 crate::mechanics::balance::tuning().dot_tick_max_pct,
201 );
202 fight::damage_entity(
203 &mut sink,
204 ctx.rng,
205 ctx.lookups,
206 ctx.entity,
207 capped,
208 custom_data,
209 )
210 .map_err(|e| anyhow::anyhow!("damage_entity: {e}"))?;
211 }
212 }
213 Ok(sink.events)
214}
215
216pub fn tutorial_buff_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
221 const MAX_CRIT: f64 = 0.3;
222 const MAX_DR: f64 = 0.6;
223
224 let event_entity_id = match ctx.caller_event {
225 Some(OverlordEvent::Damage { entity_id, .. }) => Some(*entity_id),
226 _ => None,
227 };
228 if event_entity_id != Some(ctx.entity.id) {
229 return Ok(vec![]);
230 }
231
232 let lost_hp = 1.0 - (ctx.entity.hp as f64) / (ctx.entity.max_hp as f64);
233 let crit_target = (MAX_CRIT * lost_hp * 10000.0).floor() as i64;
234 let dr_target = (MAX_DR * lost_hp * 10000.0).floor() as i64;
235
236 let buff_crit_chance = attr(ctx.entity, "tutorial_buff.crit_chance").unwrap_or(0);
237 let buff_dr = attr(ctx.entity, "tutorial_buff.dr").unwrap_or(0);
238
239 let crit_delta = crit_target - buff_crit_chance;
240 let dr_delta = dr_target - buff_dr;
241
242 let id = ctx.entity.id;
243 Ok(vec![
244 incr(id, "tutorial_buff.crit_chance", crit_delta),
245 incr(id, "crit_chance", crit_delta),
246 incr(id, "tutorial_buff.dr", dr_delta),
247 incr(id, "received_damage", -dr_delta),
248 ])
249}
250
251pub fn effect_tick_stun(_ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
264 Ok(vec![])
265}
266
267pub fn test_effect_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
270 let mut sink = NativeSink::default();
271 fight::add_entity_attr(&mut sink, ctx.entity, "test_effect_ticks", 1)
272 .map_err(|e| anyhow::anyhow!("add_entity_attr: {e}"))?;
273 Ok(sink.events)
274}
275
276pub fn bloodleak_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
278 let Some(bloodleak) = attr(ctx.entity, "bloodleak") else {
279 return Ok(vec![]);
280 };
281 Ok(vec![
282 OverlordEvent::Damage {
283 entity_id: ctx.entity.id,
284 damage: bloodleak.max(0) as u64,
285 damage_data: CustomEventData::default(),
286 },
287 incr(ctx.entity.id, "bloodleak", -5),
288 ])
289}
290
291pub fn low_hp_heal_on_damage(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
294 let Some(OverlordEvent::Damage {
295 entity_id, damage, ..
296 }) = ctx.caller_event
297 else {
298 return Ok(vec![]);
299 };
300 if *entity_id != ctx.entity.id {
301 return Ok(vec![]);
302 }
303 if ctx.entity.hp.saturating_sub(*damage) < 5 {
304 return Ok(vec![OverlordEvent::Heal {
305 entity_id: ctx.entity.id,
306 heal: 100,
307 }]);
308 }
309 Ok(vec![])
310}
311
312pub fn spawn_two_on_death(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
315 if !matches!(ctx.caller_event, Some(OverlordEvent::EntityDeath { .. })) {
316 return Ok(vec![]);
317 }
318 let template_id =
319 uuid::Uuid::parse_str("0486f548-5040-4b70-b202-8b35a9880939").expect("valid uuid literal");
320 let base = &ctx.entity.coordinates;
321 let spawn = |dy: i64| OverlordEvent::SpawnEntity {
322 id: uuid::Builder::from_random_bytes(ctx.rng.random_bytes()).into_uuid(),
323 entity_template_id: template_id,
324 position: essences::entity::Coordinates {
325 x: base.x + 1,
326 y: base.y + dy,
327 },
328 entity_team: essences::fighting::EntityTeam::Enemy,
329 has_big_hp_bar: false,
330 entity_attributes: essences::entity::EntityAttributes::default(),
331 };
332 Ok(vec![spawn(1), spawn(2)])
333}
334
335pub fn register(registry: &mut BehaviorRegistry) {
337 let mut reg = |name: &str, title: &str, desc: &str, f: EventFn| {
338 registry.register_event(
339 BehaviorMeta {
340 name: name.to_string(),
341 category: BehaviorKind::Event,
342 title: title.to_string(),
343 description: desc.to_string(),
344 },
345 f,
346 );
347 };
348 reg(
349 "protection_duration_decrement",
350 "Тик длительности protection",
351 "Уменьшает effect.protection.duration на тик (до 100), не уходя в минус.",
352 protection_duration_decrement,
353 );
354 reg(
355 "empower_duration_decrement",
356 "Тик длительности empower",
357 "Уменьшает effect.empower.duration на тик (до 100), не уходя в минус.",
358 empower_duration_decrement,
359 );
360 reg(
361 "vulnerability_duration_decrement",
362 "Тик длительности vulnerability",
363 "Уменьшает effect.vulnerability.duration на тик (до 1000), не уходя в минус.",
364 vulnerability_duration_decrement,
365 );
366 reg(
367 "weakness_duration_decrement",
368 "Тик длительности weakness",
369 "Уменьшает effect.weakness.duration на тик (до 100), не уходя в минус.",
370 weakness_duration_decrement,
371 );
372 reg(
373 "sleep_wake_tick",
374 "Тик пробуждения (sleep)",
375 "Тикает wake_up_delay; при пробуждении снимает delay/sleep.",
376 sleep_wake_tick,
377 );
378 reg(
379 "regeneration_tick",
380 "Тик регенерации",
381 "Лечит сущность на её regeneration_rate (get_entity_stat + heal_entity, без RNG).",
382 regeneration_tick,
383 );
384 reg(
385 "hot_tick",
386 "Тик HoT (лечение со временем)",
387 "Тикает 5-слотовое расписание heal-over-time: лечит на amount текущего слота, сдвигает курсор (без RNG).",
388 hot_tick,
389 );
390 reg(
391 "dot_tick",
392 "Тик DoT (урон со временем)",
393 "Тикает 5-слотовое расписание damage-over-time: наносит урон на amount текущего слота (dot/no_hit_anim), сдвигает курсор.",
394 dot_tick,
395 );
396 reg(
397 "tutorial_buff_tick",
398 "Тик обучающего баффа",
399 "При получении урона этой сущностью масштабирует crit_chance/received_damage по доле потерянного HP.",
400 tutorial_buff_tick,
401 );
402 reg(
403 "test_effect_tick",
404 "Тик тестового эффекта",
405 "Увеличивает test_effect_ticks на 1 (только для unit-теста эффекта).",
406 test_effect_tick,
407 );
408 reg(
409 "bloodleak_tick",
410 "Тик эффекта bloodleak (тест)",
411 "Если есть attr `bloodleak`: наносит урон на его величину и уменьшает \
412 `bloodleak` на 5 (только для unit-теста эффекта).",
413 bloodleak_tick,
414 );
415 reg(
416 "low_hp_heal_on_damage",
417 "Лечение при смертельном уроне (тест)",
418 "Подписан на Damage: если урон по этой сущности опускает hp ниже 5, \
419 лечит на 100 (только для unit-теста эффекта).",
420 low_hp_heal_on_damage,
421 );
422 reg(
423 "spawn_two_on_death",
424 "Спавн двух врагов при смерти (тест)",
425 "Подписан на EntityDeath: спавнит двух врагов 0486f548 рядом с сущностью \
426 (только для unit-теста эффекта).",
427 spawn_two_on_death,
428 );
429 reg(
430 "effect_tick_stun",
431 "Тик Shield/Stun",
432 "No-op: deployed Shield/Stun script вызывает незарегистрированный \
433 tick_entity_effect эффект без поведения не создаёт событий — порт \
434 возвращает пустой результат.",
435 effect_tick_stun,
436 );
437}