1use configs::game_config::GameConfig;
13use essences::combat_origin::CombatEventOrigin;
14use essences::entity::Entity;
15use essences::fight_breakdown::CombatSource;
16use essences::fighting::ActiveFight;
17use event_system::script::random::GameRng;
18
19use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
20use crate::event::CustomEventData;
21use crate::event::OverlordEvent;
22use crate::mechanics::content_lookups::ContentLookups;
23use crate::mechanics::fight::{self, NativeSink};
24
25pub struct EventCtx<'a> {
28 pub entity: &'a Entity,
29 pub fight: &'a ActiveFight,
30 pub rng: &'a GameRng,
32 pub current_tick: u64,
33 pub fight_duration_ticks: u64,
34 pub caller_event: Option<&'a OverlordEvent>,
36 pub config: &'a GameConfig,
37 pub lookups: &'a ContentLookups,
38}
39
40pub type EventFn = fn(&EventCtx) -> anyhow::Result<Vec<OverlordEvent>>;
42
43fn attr(entity: &Entity, name: &str) -> Option<i64> {
45 entity.attributes.0.get(name).copied()
46}
47
48fn incr(entity_id: uuid::Uuid, attribute: &str, delta: i64) -> OverlordEvent {
50 OverlordEvent::EntityIncrAttribute {
51 entity_id,
52 attribute: attribute.to_string(),
53 delta,
54 }
55}
56
57fn duration_decrement(ctx: &EventCtx, attr_name: &str, tick: i64) -> Vec<OverlordEvent> {
60 let Some(dur) = attr(ctx.entity, attr_name) else {
61 return vec![];
62 };
63 let delta = if dur > tick { -tick } else { -dur };
64 vec![incr(ctx.entity.id, attr_name, delta)]
65}
66
67pub fn protection_duration_decrement(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
68 Ok(duration_decrement(ctx, "effect.protection.duration", 100))
69}
70
71pub fn empower_duration_decrement(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
72 Ok(duration_decrement(ctx, "effect.empower.duration", 100))
73}
74
75pub fn war_fury_duration_decrement(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
77 Ok(duration_decrement(ctx, "effect.war_fury.duration", 100))
78}
79
80pub fn battle_blessing_duration_decrement(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
82 Ok(duration_decrement(
83 ctx,
84 "effect.battle_blessing.duration",
85 100,
86 ))
87}
88
89pub fn vulnerability_duration_decrement(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
90 Ok(duration_decrement(
91 ctx,
92 "effect.vulnerability.duration",
93 1000,
94 ))
95}
96
97pub fn weakness_duration_decrement(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
98 Ok(duration_decrement(ctx, "effect.weakness.duration", 100))
99}
100
101pub fn sleep_wake_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
105 let id = ctx.entity.id;
106 let remaining = attr(ctx.entity, "wake_up_delay").unwrap_or(0);
108 let sleep = attr(ctx.entity, "sleep");
109 let mut events = Vec::new();
110 if sleep.is_none() {
111 events.push(incr(id, "wake_up_delay", -remaining));
112 } else {
113 events.push(incr(id, "wake_up_delay", -1));
114 if remaining <= 1 {
115 events.push(incr(id, "sleep", -sleep.unwrap_or(0)));
116 }
117 }
118 Ok(events)
119}
120
121pub fn regeneration_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
128 let mut sink = NativeSink::default();
129 let percent = fight::get_entity_stat(
130 ctx.lookups,
131 ctx.entity,
132 crate::mechanics::balance::REGEN_PERCENT_CODE,
133 ) / 10_000.0;
134 let rate = fight::get_entity_stat(ctx.lookups, ctx.entity, "regeneration_rate")
135 + percent * ctx.entity.max_hp as f64;
136 let capped = crate::mechanics::balance::cap_per_tick(
140 rate,
141 ctx.entity.max_hp as f64,
142 crate::mechanics::balance::tuning().regen_tick_max_pct,
143 );
144 fight::heal_entity(
145 &mut sink,
146 Some(ctx.entity.id),
147 ctx.entity,
148 capped,
149 CombatSource::Regeneration,
150 )
151 .map_err(|e| anyhow::anyhow!("heal_entity: {e}"))?;
152 Ok(sink.events)
153}
154
155pub fn hot_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
163 let mut sink = NativeSink::default();
164 let next_attr = "effect.hot.next";
165 let next_tick_number = attr(ctx.entity, next_attr).unwrap_or(0);
167 let amount_attr = format!("effect.hot.tick.{next_tick_number}");
168 match attr(ctx.entity, &amount_attr) {
169 None => {
170 fight::set_entity_attr(&mut sink, ctx.entity, next_attr, 0.0)
171 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
172 }
173 Some(next_tick_amount) => {
174 if next_tick_number == 5 {
175 fight::set_entity_attr(&mut sink, ctx.entity, next_attr, 1.0)
176 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
177 } else {
178 fight::add_entity_attr(&mut sink, ctx.entity, next_attr, 1)
179 .map_err(|e| anyhow::anyhow!("add_entity_attr: {e}"))?;
180 }
181 fight::set_entity_attr(&mut sink, ctx.entity, &amount_attr, 0.0)
182 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
183 let capped = crate::mechanics::balance::cap_per_tick(
186 next_tick_amount as f64,
187 ctx.entity.max_hp as f64,
188 crate::mechanics::balance::tuning().hot_tick_max_pct,
189 );
190 fight::heal_entity(
194 &mut sink,
195 Some(ctx.entity.id),
196 ctx.entity,
197 capped,
198 fight::over_time_tick_source(ctx.entity, "hot", CombatSource::Hot),
199 )
200 .map_err(|e| anyhow::anyhow!("heal_entity: {e}"))?;
201 }
202 }
203 Ok(sink.events)
204}
205
206pub fn dot_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
212 let mut sink = NativeSink::default();
213 let next_attr = "effect.dot.next";
214 let next_tick_number = attr(ctx.entity, next_attr).unwrap_or(0);
215 let amount_attr = format!("effect.dot.tick.{next_tick_number}");
216 match attr(ctx.entity, &amount_attr) {
217 None => {
218 fight::set_entity_attr(&mut sink, ctx.entity, next_attr, 0.0)
219 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
220 }
221 Some(next_tick_amount) => {
222 if next_tick_number == 5 {
223 fight::set_entity_attr(&mut sink, ctx.entity, next_attr, 1.0)
224 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
225 } else {
226 fight::add_entity_attr(&mut sink, ctx.entity, next_attr, 1)
227 .map_err(|e| anyhow::anyhow!("add_entity_attr: {e}"))?;
228 }
229 let mut custom_data = CustomEventData::default();
230 custom_data.add("dot", 1);
231 custom_data.add("no_hit_anim", 1);
232 fight::set_entity_attr(&mut sink, ctx.entity, &amount_attr, 0.0)
233 .map_err(|e| anyhow::anyhow!("set_entity_attr: {e}"))?;
234 let capped = crate::mechanics::balance::cap_per_tick(
238 next_tick_amount as f64,
239 ctx.entity.max_hp as f64,
240 crate::mechanics::balance::tuning().dot_tick_max_pct,
241 );
242 fight::damage_entity(
247 &mut sink,
248 ctx.rng,
249 ctx.lookups,
250 None,
251 ctx.entity,
252 capped,
253 custom_data,
254 fight::over_time_tick_source(ctx.entity, "dot", CombatSource::Dot),
255 )
256 .map_err(|e| anyhow::anyhow!("damage_entity: {e}"))?;
257 }
258 }
259 Ok(sink.events)
260}
261
262pub fn tutorial_buff_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
267 const MAX_CRIT: f64 = 0.3;
268 const MAX_DR: f64 = 0.6;
269
270 let event_entity_id = match ctx.caller_event {
271 Some(OverlordEvent::Damage { entity_id, .. }) => Some(*entity_id),
272 _ => None,
273 };
274 if event_entity_id != Some(ctx.entity.id) {
275 return Ok(vec![]);
276 }
277
278 let lost_hp = 1.0 - (ctx.entity.hp as f64) / (ctx.entity.max_hp as f64);
279 let crit_target = (MAX_CRIT * lost_hp * 10000.0).floor() as i64;
280 let dr_target = (MAX_DR * lost_hp * 10000.0).floor() as i64;
281
282 let buff_crit_chance = attr(ctx.entity, "tutorial_buff.crit_chance").unwrap_or(0);
283 let buff_dr = attr(ctx.entity, "tutorial_buff.dr").unwrap_or(0);
284
285 let crit_delta = crit_target - buff_crit_chance;
286 let dr_delta = dr_target - buff_dr;
287
288 let id = ctx.entity.id;
289 Ok(vec![
290 incr(id, "tutorial_buff.crit_chance", crit_delta),
291 incr(id, "crit_chance", crit_delta),
292 incr(id, "tutorial_buff.dr", dr_delta),
293 incr(id, "received_damage", -dr_delta),
294 ])
295}
296
297pub fn effect_tick_stun(_ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
310 Ok(vec![])
311}
312
313pub fn test_effect_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
316 let mut sink = NativeSink::default();
317 fight::add_entity_attr(&mut sink, ctx.entity, "test_effect_ticks", 1)
318 .map_err(|e| anyhow::anyhow!("add_entity_attr: {e}"))?;
319 Ok(sink.events)
320}
321
322pub fn bloodleak_tick(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
324 let Some(bloodleak) = attr(ctx.entity, "bloodleak") else {
325 return Ok(vec![]);
326 };
327 Ok(vec![
328 OverlordEvent::Damage {
329 by_entity_id: None,
330 entity_id: ctx.entity.id,
331 damage: bloodleak.max(0) as u64,
332 damage_data: CustomEventData::default(),
333 origin: CombatEventOrigin::Core,
334 source: CombatSource::Other,
335 },
336 incr(ctx.entity.id, "bloodleak", -5),
337 ])
338}
339
340pub fn low_hp_heal_on_damage(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
343 let Some(OverlordEvent::Damage {
344 entity_id, damage, ..
345 }) = ctx.caller_event
346 else {
347 return Ok(vec![]);
348 };
349 if *entity_id != ctx.entity.id {
350 return Ok(vec![]);
351 }
352 if ctx.entity.hp.saturating_sub(*damage) < 5 {
353 return Ok(vec![OverlordEvent::Heal {
354 by_entity_id: Some(ctx.entity.id),
355 entity_id: ctx.entity.id,
356 heal: 100,
357 origin: CombatEventOrigin::Core,
358 source: CombatSource::Other,
359 }]);
360 }
361 Ok(vec![])
362}
363
364pub fn spawn_two_on_death(ctx: &EventCtx) -> anyhow::Result<Vec<OverlordEvent>> {
367 if !matches!(ctx.caller_event, Some(OverlordEvent::EntityDeath { .. })) {
368 return Ok(vec![]);
369 }
370 let template_id =
371 uuid::Uuid::parse_str("0486f548-5040-4b70-b202-8b35a9880939").expect("valid uuid literal");
372 let base = &ctx.entity.coordinates;
373 let spawn = |dy: i64| OverlordEvent::SpawnEntity {
374 id: uuid::Builder::from_random_bytes(ctx.rng.random_bytes()).into_uuid(),
375 entity_template_id: template_id,
376 position: essences::entity::Coordinates {
377 x: base.x + 1,
378 y: base.y + dy,
379 },
380 entity_team: essences::fighting::EntityTeam::Enemy,
381 has_big_hp_bar: false,
382 entity_attributes: essences::entity::EntityAttributes::default(),
383 };
384 Ok(vec![spawn(1), spawn(2)])
385}
386
387pub fn register(registry: &mut BehaviorRegistry) {
389 let mut reg = |name: &str, title: &str, desc: &str, f: EventFn| {
390 registry.register_event(
391 BehaviorMeta {
392 name: name.to_string(),
393 category: BehaviorKind::Event,
394 title: title.to_string(),
395 description: desc.to_string(),
396 },
397 f,
398 );
399 };
400 reg(
401 "protection_duration_decrement",
402 "Тик длительности protection",
403 "Уменьшает effect.protection.duration на тик (до 100), не уходя в минус.",
404 protection_duration_decrement,
405 );
406 reg(
407 "empower_duration_decrement",
408 "Тик длительности empower",
409 "Уменьшает effect.empower.duration на тик (до 100), не уходя в минус.",
410 empower_duration_decrement,
411 );
412 reg(
413 "war_fury_duration_decrement",
414 "Тик длительности war_fury",
415 "Уменьшает effect.war_fury.duration на тик (до 100), не уходя в минус.",
416 war_fury_duration_decrement,
417 );
418 reg(
419 "battle_blessing_duration_decrement",
420 "Тик длительности battle_blessing",
421 "Уменьшает effect.battle_blessing.duration на тик (до 100), не уходя в минус.",
422 battle_blessing_duration_decrement,
423 );
424 reg(
425 "vulnerability_duration_decrement",
426 "Тик длительности vulnerability",
427 "Уменьшает effect.vulnerability.duration на тик (до 1000), не уходя в минус.",
428 vulnerability_duration_decrement,
429 );
430 reg(
431 "weakness_duration_decrement",
432 "Тик длительности weakness",
433 "Уменьшает effect.weakness.duration на тик (до 100), не уходя в минус.",
434 weakness_duration_decrement,
435 );
436 reg(
437 "sleep_wake_tick",
438 "Тик пробуждения (sleep)",
439 "Тикает wake_up_delay; при пробуждении снимает delay/sleep.",
440 sleep_wake_tick,
441 );
442 reg(
443 "regeneration_tick",
444 "Тик регенерации",
445 "Лечит сущность на её regeneration_rate (get_entity_stat + heal_entity, без RNG).",
446 regeneration_tick,
447 );
448 reg(
449 "hot_tick",
450 "Тик HoT (лечение со временем)",
451 "Тикает 5-слотовое расписание heal-over-time: лечит на amount текущего слота, сдвигает курсор (без RNG).",
452 hot_tick,
453 );
454 reg(
455 "dot_tick",
456 "Тик DoT (урон со временем)",
457 "Тикает 5-слотовое расписание damage-over-time: наносит урон на amount текущего слота (dot/no_hit_anim), сдвигает курсор.",
458 dot_tick,
459 );
460 reg(
461 "tutorial_buff_tick",
462 "Тик обучающего баффа",
463 "При получении урона этой сущностью масштабирует crit_chance/received_damage по доле потерянного HP.",
464 tutorial_buff_tick,
465 );
466 reg(
467 "test_effect_tick",
468 "Тик тестового эффекта",
469 "Увеличивает test_effect_ticks на 1 (только для unit-теста эффекта).",
470 test_effect_tick,
471 );
472 reg(
473 "bloodleak_tick",
474 "Тик эффекта bloodleak (тест)",
475 "Если есть attr `bloodleak`: наносит урон на его величину и уменьшает \
476 `bloodleak` на 5 (только для unit-теста эффекта).",
477 bloodleak_tick,
478 );
479 reg(
480 "low_hp_heal_on_damage",
481 "Лечение при смертельном уроне (тест)",
482 "Подписан на Damage: если урон по этой сущности опускает hp ниже 5, \
483 лечит на 100 (только для unit-теста эффекта).",
484 low_hp_heal_on_damage,
485 );
486 reg(
487 "spawn_two_on_death",
488 "Спавн двух врагов при смерти (тест)",
489 "Подписан на EntityDeath: спавнит двух врагов 0486f548 рядом с сущностью \
490 (только для unit-теста эффекта).",
491 spawn_two_on_death,
492 );
493 reg(
494 "effect_tick_stun",
495 "Тик Shield/Stun",
496 "No-op: deployed Shield/Stun script вызывает незарегистрированный \
497 tick_entity_effect эффект без поведения не создаёт событий — порт \
498 возвращает пустой результат.",
499 effect_tick_stun,
500 );
501}