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