essences/
fighting.rs

1use crate::abilities::AbilityId;
2use crate::bundles::BundleId;
3use crate::dungeons::DungeonTemplateId;
4use crate::entity::{Coordinates, Entity, EntityId};
5use crate::game::EntityTemplateId;
6use crate::pets::PetId;
7
8use crate::prelude::*;
9
10use strum_macros::{Display, EnumString};
11
12#[derive(
13    Clone,
14    Debug,
15    Default,
16    Serialize,
17    Deserialize,
18    PartialEq,
19    Eq,
20    Tsify,
21    EnumString,
22    Display,
23    JsonSchema,
24)]
25#[tsify(from_wasm_abi, into_wasm_abi)]
26pub enum EntityTeam {
27    #[default]
28    Ally,
29    Enemy,
30}
31
32#[derive(
33    Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, Display, JsonSchema,
34)]
35#[tsify(from_wasm_abi, into_wasm_abi)]
36pub enum EntityType {
37    #[schemars(title = "ПВЕ юнит")]
38    PVEEntity {
39        #[schemars(title = "ID юнита", schema_with = "entity_link_id_schema")]
40        entity_template_id: EntityTemplateId,
41    },
42    #[default]
43    #[schemars(title = "ПВП юнит")]
44    PVPEntity,
45}
46
47#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
48pub struct FightEntity {
49    #[schemars(title = "Тип юнита")]
50    pub entity_type: EntityType,
51    #[schemars(title = "Координаты юнита")]
52    pub position: Coordinates,
53    #[schemars(title = "Нужна ли отрисовка большого хп бара")]
54    pub has_big_hp_bar: bool,
55    #[schemars(title = "Команда юнита")]
56    pub team: EntityTeam,
57}
58
59#[derive(
60    Clone,
61    Debug,
62    Default,
63    Serialize,
64    Deserialize,
65    PartialEq,
66    Eq,
67    Tsify,
68    EnumString,
69    Display,
70    JsonSchema,
71)]
72#[tsify(from_wasm_abi, into_wasm_abi)]
73pub enum FightType {
74    #[default]
75    CampaignFight,
76    CampaignBossFight,
77    ArenaPVP,
78    VassalPVP,
79    SingleFight,
80}
81
82#[declare]
83pub type FightTemplateId = Uuid;
84
85/// Typed, config-level mirror of `overlord_event_system`'s `WaveFightData` (the
86/// shape `prepare_fight_script` builds inline and passes to `ctx.spawn_wave`).
87/// Holds the wave data as typed config — pure data, no combat and no RNG. Field
88/// optionality mirrors `WaveFightData` exactly.
89#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Tsify, JsonSchema)]
90pub struct PrepareFightWaves {
91    #[schemars(title = "Относительные силы шаблонов сущностей")]
92    pub entities: Vec<PrepareFightEntityPower>,
93    #[schemars(title = "Волны спавна")]
94    pub waves: Vec<Vec<PrepareFightSpawn>>,
95    #[schemars(title = "Бюджет времени боя (сек), для нормализации HP мобов")]
96    pub time: f64,
97    #[serde(default)]
98    #[schemars(title = "Опорная сила волны (base_power для spawn_wave)")]
99    pub power: f64,
100}
101
102#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Tsify, JsonSchema)]
103pub struct PrepareFightEntityPower {
104    // Entity-link so the admin resolves `$ref(entities/Name~uuid)` → bare uuid in
105    // game_config.json, matching how the runtime `prepare_fight_script` sees it.
106    #[serde(default)]
107    #[schemars(schema_with = "option_entity_link_id_schema")]
108    pub entity_id: Option<String>,
109    #[serde(default)]
110    pub power: Option<f64>,
111}
112
113#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Tsify, JsonSchema)]
114pub struct PrepareFightSpawn {
115    #[schemars(schema_with = "entity_link_id_schema")]
116    pub entity_id: String,
117    #[serde(default)]
118    pub delay: Option<f64>,
119    #[serde(default)]
120    pub position: Option<Coordinates>,
121}
122
123// NOTE: `Eq` intentionally dropped — `prepare_fight_waves` carries f64 powers/
124// time/delays (not `Eq`). `FightTemplate` is never used as a hash/set key (only
125// `FightTemplateId` is), so `PartialEq` suffices.
126#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Tsify, JsonSchema)]
127pub struct FightTemplate {
128    #[schemars(schema_with = "id_schema")]
129    pub id: FightTemplateId,
130
131    #[schemars(title = "Название боя")]
132    pub title: i18n::I18nString,
133
134    #[schemars(title = "Мощь боя")]
135    pub power: Option<u64>,
136
137    // TODO #[schemars(schema_with = "battlefield_id_schema")]
138    // pub battlefield: BattlefieldId, // TODO mb location, not battlefield?
139    #[schemars(title = "Все существа в бою")]
140    pub fight_entities: Vec<FightEntity>,
141
142    #[schemars(title = "Максимальная длительность боя в тиках")]
143    pub max_duration_ticks: u64,
144
145    #[schemars(title = "Целевая ширина экрана (в клетках)")]
146    pub target_width_cells: u64,
147
148    #[schemars(title = "Fx на старте боя")]
149    // TODO needs implementation + maybe add delay ticks, so fx would have enought time to play?
150    pub starting_fx: String,
151
152    #[schemars(title = "Тип боя")]
153    pub fight_type: FightType,
154
155    #[schemars(title = "Количество волн в бою")]
156    pub waves_amount: i64,
157
158    #[schemars(
159        title = "Волны боя (типизированные)",
160        description = "Типизированные данные волн боя."
161    )]
162    #[serde(default)]
163    pub prepare_fight_waves: Option<PrepareFightWaves>,
164
165    #[schemars(
166        title = "Нативная функция начала боя",
167        description = "Имя нативной функции категории fight_start, выполняющей начало боя.",
168        schema_with = "fight_start_ref_schema"
169    )]
170    #[serde(default)]
171    pub start_behavior: Option<String>,
172
173    #[schemars(title = "Задник", schema_with = "asset_background_schema")]
174    pub background: String,
175
176    #[schemars(
177        title = "Время ожидания перед стартом следующего боя в тиках",
178        description = "Это момент анимации перебежки персонажа между битвами"
179    )]
180    pub start_fight_delay_ticks: Option<u64>,
181
182    #[schemars(
183        title = "Время ожидания перед стартом уже начавшегося боя в случае победы",
184        description = "Это момент, когда враги выбегают на экран"
185    )]
186    pub prepare_fight_win_duration_ticks: Option<u64>,
187
188    #[schemars(
189        title = "Время ожидания перед стартом уже начавшегося боя в случае поражения",
190        description = "Это момент, когда враги выбегают на экран"
191    )]
192    pub prepare_fight_lose_duration_ticks: Option<u64>,
193
194    #[schemars(title = "Время ожидания перед окончанием боя")]
195    pub end_fight_delay_ticks: Option<u64>,
196
197    #[schemars(
198        title = "Бандл награды за победу в бою",
199        schema_with = "option_bundle_id_schema"
200    )]
201    pub bundle_reward_id: Option<BundleId>,
202
203    #[schemars(title = "Останавливать ли бой после победы")]
204    pub stop_on_win: bool,
205
206    #[schemars(title = "Останавливать ли бой после поражения")]
207    pub stop_on_lose: bool,
208
209    #[schemars(title = "Показывать ли VS экран в начале боя")]
210    pub show_vs_screen: bool,
211
212    #[schemars(title = "Показывать ли стейджи")]
213    pub show_stages: Option<bool>,
214
215    #[schemars(
216        title = "Является ли подземельем",
217        description = "Бой подземелья (fight_template_is_dungeon): включает dungeon-таланты в init_fight."
218    )]
219    #[serde(default)]
220    pub is_dungeon: bool,
221
222    #[schemars(
223        title = "Является ли боссфайтом",
224        description = "Босс-бой (fight_template_is_bossfight): включает boss-таланты в init_fight."
225    )]
226    #[serde(default)]
227    pub is_bossfight: bool,
228}
229
230#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
231pub struct ActiveDungeon {
232    pub id: DungeonTemplateId,
233    pub difficulty: i64,
234}
235
236#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
237pub struct ActivePetAbility {
238    pub pet_template_id: PetId,
239    pub ability_id: AbilityId,
240    pub charge: i64,
241    pub max_charge: i64,
242}
243
244#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
245pub struct ActiveFight {
246    pub id: uuid::Uuid,
247    pub fight_id: FightTemplateId,
248    pub current_wave: i64,
249    pub player_id: EntityId,
250    pub party_player_id: Option<EntityId>,
251    pub entities: Vec<Entity>,
252    pub fight_stopped: bool,
253    pub fight_ended: bool,
254    pub max_duration_ticks: u64,
255    pub dungeon: Option<ActiveDungeon>,
256    pub paused: bool,
257    pub pet_combat_state: Option<ActivePetAbility>,
258    pub leader_pet_template_id: Option<PetId>,
259}
260
261impl ActiveFight {
262    pub fn get_player(&self) -> Option<&Entity> {
263        self.entities
264            .iter()
265            .find(|entity| entity.id == self.player_id)
266    }
267
268    pub fn get_player_mut(&mut self) -> Option<&mut Entity> {
269        self.entities
270            .iter_mut()
271            .find(|entity| entity.id == self.player_id)
272    }
273
274    pub fn get_party_player(&self) -> Option<&Entity> {
275        let party_id = self.party_player_id?;
276        self.entities.iter().find(|entity| entity.id == party_id)
277    }
278
279    pub fn get_party_player_mut(&mut self) -> Option<&mut Entity> {
280        let party_id = self.party_player_id?;
281        self.entities
282            .iter_mut()
283            .find(|entity| entity.id == party_id)
284    }
285
286    pub fn get_enemies_amount(&self) -> usize {
287        self.entities
288            .iter()
289            .filter(|entity| entity.team == EntityTeam::Enemy)
290            .count()
291    }
292}