1use configs::abilities::ProjectileId;
14use configs::game_config::GameConfig;
15use essences::abilities::AbilityId;
16use essences::combat_origin::CombatEventOrigin;
17use essences::entity::Entity;
18use essences::entity::{ActionWithDeadline, Coordinates, EntityAction, EntityId};
19use essences::fighting::ActiveFight;
20use event_system::event::EventPluginized;
21use event_system::script::random::GameRng;
22use serde::Serialize;
23
24use crate::event::{CustomEventData, OverlordEvent};
25use crate::game_config_helpers::GameConfigLookup;
26use crate::state::OverlordState;
27use uuid::Uuid;
28
29use crate::behaviors::{BehaviorKind, BehaviorMeta, BehaviorRegistry};
30use crate::mechanics::content_lookups::ContentLookups;
31use crate::mechanics::fight::{self, NativeSink};
32
33pub struct StartCastAbilityCtx<'a> {
36 pub caster: &'a Entity,
37 pub fight: &'a ActiveFight,
38 pub rng: &'a GameRng,
40 pub ability_template_id: Uuid,
43 pub config: &'a GameConfig,
44 pub lookups: &'a ContentLookups,
45}
46
47pub type StartCastAbilityFn =
49 fn(&StartCastAbilityCtx) -> anyhow::Result<Vec<StartCastAbilityResult>>;
50
51pub fn try_cast_self(ctx: &StartCastAbilityCtx) -> anyhow::Result<Vec<StartCastAbilityResult>> {
55 let mut sink = NativeSink::default();
56 fight::try_cast(
57 &mut sink,
58 ctx.rng,
59 ctx.config,
60 ctx.lookups,
61 ctx.fight,
62 ctx.caster,
63 ctx.ability_template_id,
64 1,
65 )
66 .map_err(|e| anyhow::anyhow!("try_cast: {e}"))?;
67 StartCastAbilityResult::vec_from_script_results(&sink.casts)
68}
69
70pub fn self_attack_400(ctx: &StartCastAbilityCtx) -> anyhow::Result<Vec<StartCastAbilityResult>> {
74 Ok(vec![StartCastAbilityResult::Attack {
75 delay_ticks: 0,
76 animation_duration_ticks: 400,
77 target_entity_id: ctx.caster.id,
78 origin: CombatEventOrigin::Core,
79 }])
80}
81
82pub fn attack_first_enemy(
85 ctx: &StartCastAbilityCtx,
86) -> anyhow::Result<Vec<StartCastAbilityResult>> {
87 let Some(target) = ctx
88 .fight
89 .entities
90 .iter()
91 .find(|e| e.team != ctx.caster.team)
92 else {
93 return Ok(vec![]);
94 };
95 Ok(vec![StartCastAbilityResult::Attack {
96 delay_ticks: 0,
97 animation_duration_ticks: 500,
98 target_entity_id: target.id,
99 origin: CombatEventOrigin::Core,
100 }])
101}
102
103pub fn self_attack_500(ctx: &StartCastAbilityCtx) -> anyhow::Result<Vec<StartCastAbilityResult>> {
107 Ok(vec![StartCastAbilityResult::Attack {
108 delay_ticks: 0,
109 animation_duration_ticks: 500,
110 target_entity_id: ctx.caster.id,
111 origin: CombatEventOrigin::Core,
112 }])
113}
114
115fn register_ability_fns(registry: &mut BehaviorRegistry) {
117 registry.register_start_cast_ability(
118 BehaviorMeta {
119 name: "try_cast_self".to_string(),
120 category: BehaviorKind::StartCastAbility,
121 title: "Авто-каст своей абилки".to_string(),
122 description: "Порт start_behavior `ctx.try_cast(CasterEntity, <self ability id>)` — \
123 кастует абилку кастера через try_cast (casts=1)."
124 .to_string(),
125 },
126 try_cast_self,
127 );
128 registry.register_start_cast_ability(
129 BehaviorMeta {
130 name: "self_attack_400".to_string(),
131 category: BehaviorKind::StartCastAbility,
132 title: "Само-атака (anim 400)".to_string(),
133 description: "Порт `Result.push_attack(0, 400, CasterEntity.id)` — одиночная \
134 атака по себе, без RNG."
135 .to_string(),
136 },
137 self_attack_400,
138 );
139 registry.register_start_cast_ability(
140 BehaviorMeta {
141 name: "attack_first_enemy".to_string(),
142 category: BehaviorKind::StartCastAbility,
143 title: "Атака по первому врагу (anim 500)".to_string(),
144 description: "Порт общего test start_behavior: атака по первой сущности \
145 вражеской команды, anim 500."
146 .to_string(),
147 },
148 attack_first_enemy,
149 );
150 registry.register_start_cast_ability(
151 BehaviorMeta {
152 name: "self_attack_500".to_string(),
153 category: BehaviorKind::StartCastAbility,
154 title: "Само-атака (anim 500)".to_string(),
155 description: "Порт `Result.push_attack(0, 500, CasterEntity.id)` \
156 (test ability 41ee5532)."
157 .to_string(),
158 },
159 self_attack_500,
160 );
161}
162pub struct StartCastProjectileCtx<'a> {
198 pub caster_entity: &'a Entity,
199 pub target_entity: &'a Entity,
200}
201
202pub type StartCastProjectileFn =
206 fn(&StartCastProjectileCtx) -> anyhow::Result<StartCastProjectileResult>;
207
208fn caster_target_distance(ctx: &StartCastProjectileCtx) -> f64 {
211 let dx = ctx.caster_entity.coordinates.x - ctx.target_entity.coordinates.x;
212 let dy = ctx.caster_entity.coordinates.y - ctx.target_entity.coordinates.y;
213 let x2 = dx * dx;
215 let y2 = dy * dy;
216 ((x2 + y2) as f64).powf(0.5)
218}
219
220fn distance_animation(
223 ctx: &StartCastProjectileCtx,
224 multiplier: f64,
225) -> anyhow::Result<StartCastProjectileResult> {
226 let distance = caster_target_distance(ctx);
227 let ticks = (distance * multiplier).floor() as i64 as u64;
230 Ok(StartCastProjectileResult {
231 projectile_data: Default::default(),
232 animation_duration_ticks: ticks,
233 })
234}
235
236pub fn distance_x100(ctx: &StartCastProjectileCtx) -> anyhow::Result<StartCastProjectileResult> {
238 distance_animation(ctx, 100.0)
239}
240
241pub fn distance_x380(ctx: &StartCastProjectileCtx) -> anyhow::Result<StartCastProjectileResult> {
244 distance_animation(ctx, 380.0)
245}
246
247pub fn fixed_200(_ctx: &StartCastProjectileCtx) -> anyhow::Result<StartCastProjectileResult> {
250 Ok(StartCastProjectileResult {
251 projectile_data: Default::default(),
252 animation_duration_ticks: 200,
253 })
254}
255
256pub fn fixed_500_damage_300(
261 _ctx: &StartCastProjectileCtx,
262) -> anyhow::Result<StartCastProjectileResult> {
263 let mut projectile_data = crate::event::CustomEventData::default();
264 projectile_data.add("damage", 300);
265 Ok(StartCastProjectileResult {
266 projectile_data,
267 animation_duration_ticks: 500,
268 })
269}
270
271fn register_projectile_fns(registry: &mut BehaviorRegistry) {
273 registry.register_start_cast_projectile(
274 BehaviorMeta {
275 name: "projectile_fixed_500_damage_300".to_string(),
276 category: BehaviorKind::StartCastProjectile,
277 title: "Снаряд: 500 тиков + damage=300 (тест)".to_string(),
278 description: "Фиксированные 500 тиков анимации; projectile_data = {damage: 300} \
279 (порт test projectile start_behavior)."
280 .to_string(),
281 },
282 fixed_500_damage_300,
283 );
284 registry.register_start_cast_projectile(
285 BehaviorMeta {
286 name: "projectile_distance_x100".to_string(),
287 category: BehaviorKind::StartCastProjectile,
288 title: "Снаряд: длительность по дистанции (×100)".to_string(),
289 description: "unsigned(floor(дистанция_кастер_цель * 100)) тиков анимации; \
290 projectile_data пустой (порт projectile start_behavior ×100)."
291 .to_string(),
292 },
293 distance_x100,
294 );
295 registry.register_start_cast_projectile(
296 BehaviorMeta {
297 name: "projectile_distance_x380".to_string(),
298 category: BehaviorKind::StartCastProjectile,
299 title: "Снаряд: длительность по дистанции (×380)".to_string(),
300 description: "unsigned(floor(дистанция_кастер_цель * 380)) тиков анимации; \
301 projectile_data пустой (порт projectile start_behavior ×380)."
302 .to_string(),
303 },
304 distance_x380,
305 );
306 registry.register_start_cast_projectile(
307 BehaviorMeta {
308 name: "projectile_fixed_200".to_string(),
309 category: BehaviorKind::StartCastProjectile,
310 title: "Снаряд: фиксированная длительность 200".to_string(),
311 description: "Фиксированные 200 тиков анимации; projectile_data пустой \
312 (порт projectile start_behavior unsigned(200))."
313 .to_string(),
314 },
315 fixed_200,
316 );
317}
318
319pub fn register(registry: &mut BehaviorRegistry) {
321 register_ability_fns(registry);
322 register_projectile_fns(registry);
323}
324
325#[derive(Debug, Clone, Default, PartialEq, Eq)]
330pub struct StartCastAbilityScriptResult {
331 pub delay_ticks: Option<u64>,
332 pub animation_duration_ticks: Option<u64>,
333 pub target_entity_id: Option<Uuid>,
334
335 pub coordinates: Option<Coordinates>,
336 pub run_duration_ticks: Option<u64>,
337
338 pub origin: CombatEventOrigin,
344}
345
346#[derive(Clone, Debug, PartialEq, serde::Serialize)]
347pub enum StartCastAbilityResult {
348 Run {
349 coordinates: Coordinates,
350 run_duration_ticks: u64,
351 origin: CombatEventOrigin,
353 },
354 Attack {
355 delay_ticks: u64,
356 animation_duration_ticks: u64,
357 target_entity_id: Uuid,
358 origin: CombatEventOrigin,
360 },
361 None,
362}
363
364impl StartCastAbilityResult {
365 pub fn vec_from_script_results(
369 results: &[StartCastAbilityScriptResult],
370 ) -> anyhow::Result<Vec<StartCastAbilityResult>> {
371 let mut converted_results = Vec::new();
372 let mut running = false;
373 for result in results.iter().cloned() {
374 if result.run_duration_ticks.is_some() && result.animation_duration_ticks.is_some() {
375 anyhow::bail!(
376 "Attack and run provided in one singular result {:?}",
377 result
378 )
379 }
380 let converted_result = if let (Some(run_duration_ticks), Some(coordinates)) =
381 (result.run_duration_ticks, result.coordinates)
382 {
383 running = true;
384 StartCastAbilityResult::Run {
385 coordinates,
386 run_duration_ticks,
387 origin: result.origin,
388 }
389 } else if let (
390 Some(animation_duration_ticks),
391 Some(target_entity_id),
392 Some(delay_ticks),
393 ) = (
394 result.animation_duration_ticks,
395 result.target_entity_id,
396 result.delay_ticks,
397 ) {
398 StartCastAbilityResult::Attack {
399 delay_ticks,
400 animation_duration_ticks,
401 target_entity_id,
402 origin: result.origin,
403 }
404 } else {
405 StartCastAbilityResult::None
406 };
407 converted_results.push(converted_result);
408 }
409 if running && converted_results.len() > 1 {
410 anyhow::bail!("More than 1 result in start_cast_ability with running {results:?}")
411 }
412 Ok(converted_results)
413 }
414
415 pub fn into_entity_action_with_deadline(
420 &self,
421 class_id: Uuid,
422 game_config: &GameConfig,
423 ability_id: AbilityId,
424 current_tick: u64,
425 dispatch_origin: CombatEventOrigin,
426 ) -> anyhow::Result<ActionWithDeadline> {
427 match self {
428 StartCastAbilityResult::Run { .. } => {
429 anyhow::bail!("Got Run for StartCastAbilityResult into_entity_action")
430 }
431 StartCastAbilityResult::Attack {
432 delay_ticks,
433 animation_duration_ticks,
434 target_entity_id,
435 origin,
436 } => {
437 let Some(class) = game_config.class(class_id) else {
438 anyhow::bail!("Failed to get class with id: {}", class_id);
439 };
440
441 let action = if !class.basic_abilities.contains(&ability_id) {
442 EntityAction::CastAbility {
443 ability_id,
444 target_entity_id: *target_entity_id,
445 }
446 } else {
447 EntityAction::CastBasicAbility {
448 ability_id,
449 target_entity_id: *target_entity_id,
450 }
451 };
452
453 Ok(ActionWithDeadline {
454 action,
455 deadline_tick: current_tick + *delay_ticks + *animation_duration_ticks,
456 origin: origin.merge(dispatch_origin),
457 })
458 }
459 StartCastAbilityResult::None => {
460 anyhow::bail!("Got NONE for StartCastAbilityResult into_entity_action")
461 }
462 }
463 }
464
465 pub fn into_event(
466 &self,
467 ability_id: AbilityId,
468 by_entity_id: EntityId,
469 dispatch_origin: CombatEventOrigin,
470 ) -> Option<EventPluginized<OverlordEvent, OverlordState>> {
471 match self {
472 StartCastAbilityResult::Run {
473 coordinates,
474 run_duration_ticks,
475 ..
476 } => Some(EventPluginized::now(OverlordEvent::StartMove {
477 entity_id: by_entity_id,
478 to: coordinates.clone(),
479 duration_ticks: *run_duration_ticks,
480 })),
481 StartCastAbilityResult::Attack {
482 delay_ticks,
483 animation_duration_ticks,
484 origin,
485 ..
486 } => Some(EventPluginized::delayed(
487 OverlordEvent::StartedCastAbility {
488 by_entity_id,
489 ability_id,
490 duration_ticks: *animation_duration_ticks,
491 origin: origin.merge(dispatch_origin),
492 },
493 *delay_ticks,
494 )),
495 StartCastAbilityResult::None => None,
496 }
497 }
498
499 #[allow(clippy::type_complexity)]
500 pub fn vec_into_actions_with_deadlines_and_events(
501 results: &Vec<StartCastAbilityResult>,
502 class_id: Uuid,
503 game_config: &GameConfig,
504 ability_id: AbilityId,
505 entity_id: EntityId,
506 current_tick: u64,
507 dispatch_origin: CombatEventOrigin,
508 ) -> anyhow::Result<(
509 Vec<ActionWithDeadline>,
510 Vec<EventPluginized<OverlordEvent, OverlordState>>,
511 )> {
512 let mut actions = vec![];
513 let mut events = vec![];
514
515 for result in results {
516 if matches!(result, StartCastAbilityResult::Attack { .. }) {
517 actions.push(result.into_entity_action_with_deadline(
518 class_id,
519 game_config,
520 ability_id,
521 current_tick,
522 dispatch_origin,
523 )?);
524 }
525
526 if let Some(event) = result.into_event(ability_id, entity_id, dispatch_origin) {
527 events.push(event);
528 }
529 }
530
531 Ok((actions, events))
532 }
533}
534
535#[derive(Debug, Clone, Default, PartialEq, Serialize)]
536pub struct StartCastProjectileResult {
537 pub projectile_data: CustomEventData,
538 pub animation_duration_ticks: u64,
539}
540
541impl StartCastProjectileResult {
542 pub fn into_frontend_event(
543 &self,
544 by_entity_id: EntityId,
545 to_entity_id: EntityId,
546 projectile_id: ProjectileId,
547 source: essences::fight_breakdown::CombatSource,
548 ) -> EventPluginized<OverlordEvent, OverlordState> {
549 EventPluginized::now(OverlordEvent::StartedCastProjectile {
550 by_entity_id,
551 to_entity_id,
552 projectile_id,
553 duration_ticks: self.animation_duration_ticks,
554 origin: CombatEventOrigin::Core,
555 source,
556 })
557 }
558}