tiger_lib/ck3/data/
greatprojects.rs

1use crate::block::{BV, Block};
2use crate::ck3::tables::misc::PROVINCE_FILTERS;
3use crate::ck3::validate::validate_cost;
4use crate::context::ScopeContext;
5use crate::db::{Db, DbKind};
6use crate::desc::validate_desc;
7use crate::everything::Everything;
8use crate::game::GameFlags;
9use crate::item::{Item, ItemLoader};
10use crate::report::{ErrorKey, warn};
11use crate::scopes::Scopes;
12use crate::token::Token;
13use crate::tooltipped::Tooltipped;
14use crate::validator::Validator;
15
16#[derive(Clone, Debug)]
17pub struct GreatProjectType {}
18
19inventory::submit! {
20    ItemLoader::Normal(GameFlags::Ck3, Item::GreatProjectType, GreatProjectType::add)
21}
22
23impl GreatProjectType {
24    pub fn add(db: &mut Db, key: Token, block: Block) {
25        db.add(Item::GreatProjectType, key, block, Box::new(Self {}));
26    }
27}
28
29impl DbKind for GreatProjectType {
30    fn add_subitems(&self, _key: &Token, block: &Block, db: &mut Db) {
31        if let Some(block) = block.get_field_block("project_contributions") {
32            for (key, _) in block.iter_definitions() {
33                db.add_flag(Item::ProjectContribution, key.clone());
34            }
35        }
36    }
37
38    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
39        let loca = format!("great_project_type_{key}");
40        data.verify_exists_implied(Item::Localization, &loca, key);
41        let loca = format!("great_project_type_tooltip_{key}");
42        data.verify_exists_implied(Item::Localization, &loca, key);
43        let loca = format!("great_project_name_{key}");
44        data.localization.suggest(&loca, key);
45        let loca = format!("great_project_name_possessive_{key}");
46        data.localization.suggest(&loca, key);
47
48        let mut vd = Validator::new(block, data);
49
50        if !block.has_key("icon") {
51            data.verify_icon("NGameIcons|GREAT_PROJECT_TYPE_ICON_PATH", key, ".dds");
52        }
53
54        vd.multi_field_validated("icon", |bv, data| match bv {
55            BV::Value(value) => {
56                data.verify_exists(Item::File, value);
57            }
58            BV::Block(block) => {
59                let mut vd = Validator::new(block, data);
60                vd.field_trigger_builder("trigger", Tooltipped::No, |key| {
61                    let mut sc = ScopeContext::new(Scopes::GreatProject, key);
62                    sc.define_name("province", Scopes::Province, key);
63                    sc.define_name("great_project", Scopes::GreatProject, key);
64                    sc.define_name("owner", Scopes::Character, key);
65                    sc.define_name("founder", Scopes::Character, key);
66                    sc
67                });
68                vd.field_item("reference", Item::File);
69            }
70        });
71
72        vd.multi_field_validated_block("illustration", |block, data| {
73            let mut vd = Validator::new(block, data);
74            vd.field_trigger_builder("trigger", Tooltipped::No, |key| {
75                let mut sc = ScopeContext::new(Scopes::GreatProject, key);
76                sc.define_name("province", Scopes::Province, key);
77                sc.define_name("great_project", Scopes::GreatProject, key);
78                sc.define_name("owner", Scopes::Character, key);
79                sc.define_name("founder", Scopes::Character, key);
80                sc
81            });
82            vd.field_item("reference", Item::File);
83        });
84
85        // docs say it's like the previous 2 fields, but it seems to be a normal triggered desc.
86        vd.field_validated_key("name", |key, bv, data| {
87            let mut sc = ScopeContext::new(Scopes::Character, key);
88            validate_desc(bv, data, &mut sc);
89        });
90
91        vd.field_trigger_builder("is_shown", Tooltipped::No, |key| {
92            let mut sc = ScopeContext::new(Scopes::Character, key);
93            sc.define_name("province", Scopes::Province, key);
94            sc.define_name("founder", Scopes::Character, key);
95            sc
96        });
97
98        vd.field_choice("province_filter", PROVINCE_FILTERS);
99        if let Some(filter) = block.get_field_value("province_filter") {
100            if filter.is("landed_title") {
101                vd.field_item("province_filter_target", Item::Title);
102            } else if filter.is("geographical_region") {
103                vd.field_item("province_filter_target", Item::Region);
104            } else {
105                vd.ban_field(
106                    "province_filter_target",
107                    || "`landed_title` or `geographical_region` filters",
108                );
109            }
110        }
111
112        vd.field_trigger_builder("can_start_planning", Tooltipped::Yes, |key| {
113            let mut sc = ScopeContext::new(Scopes::Character, key);
114            sc.define_name("founder", Scopes::Character, key);
115            sc
116        });
117        vd.field_trigger_rooted("can_cancel", Tooltipped::Yes, Scopes::Character);
118        vd.field_trigger_builder("is_location_valid", Tooltipped::Yes, |key| {
119            let mut sc = ScopeContext::new(Scopes::Character, key);
120            sc.define_name("province", Scopes::Province, key);
121            sc.define_name("founder", Scopes::Character, key);
122            sc
123        });
124
125        vd.field_choice(
126            "owner",
127            &[
128                "province_owner_top_liege",
129                "province_owner",
130                "founder_primary_title_owner",
131                "founder_top_liege_title_owner",
132            ],
133        );
134        vd.field_trigger_builder("is_valid", Tooltipped::No, |key| {
135            let mut sc = ScopeContext::new(Scopes::GreatProject, key);
136            sc.define_name("province", Scopes::Province, key);
137            sc.define_name("great_project", Scopes::GreatProject, key);
138            sc.define_name("owner", Scopes::Character, key);
139            sc.define_name("founder", Scopes::Character, key);
140            sc
141        });
142        vd.field_validated_key_block("cost", |key, block, data| {
143            let mut sc = ScopeContext::new(Scopes::Character, key);
144            validate_cost(block, data, &mut sc);
145        });
146
147        vd.field_script_value_builder("construction_time", |key| {
148            let mut sc = ScopeContext::new(Scopes::Character, key);
149            sc.define_name("great_project", Scopes::GreatProject, key);
150            sc
151        });
152        vd.field_numeric("contribution_threshold");
153        vd.advice_field(
154            "investor_cooldown",
155            "docs say `investor_cooldown` but it's `contributor_cooldown`",
156        );
157        vd.field_script_value_builder("contributor_cooldown", |key| {
158            let mut sc = ScopeContext::new(Scopes::Character, key);
159            sc.define_name("great_project", Scopes::GreatProject, key);
160            sc
161        });
162
163        for field in
164            &["on_complete", "on_cancel", "on_plan_build", "on_start_build", "on_invalidated"]
165        {
166            vd.field_effect_builder(field, Tooltipped::Yes, |key| {
167                let mut sc = ScopeContext::new(Scopes::GreatProject, key);
168                sc.define_name("province", Scopes::Province, key);
169                sc.define_name("great_project", Scopes::GreatProject, key);
170                sc.define_name("owner", Scopes::Character, key);
171                sc.define_name("founder", Scopes::Character, key);
172                sc
173            });
174        }
175
176        // TODO: the interaction must use the request_great_project_contribution special interaction type
177        vd.field_item("invite_interaction", Item::CharacterInteraction);
178
179        vd.field_validated_block("allowed_contributor_filter", validate_contributor_filter);
180
181        vd.field_validated_block("project_contributions", |block, data| {
182            let mut vd = Validator::new(block, data);
183            vd.unknown_block_fields(|k, block| {
184                validate_contribution(k, block, data, key);
185            });
186        });
187
188        vd.field_script_value_no_breakdown_rooted("ai_will_do", Scopes::Character);
189        vd.field_choice("ai_province_filter", PROVINCE_FILTERS);
190        vd.field_integer("ai_check_interval");
191        vd.field_validated_key_block("ai_check_interval_by_tier", |key, b, data| {
192            let mut vd = Validator::new(b, data);
193            for tier in &["barony", "county", "duchy", "kingdom", "empire", "hegemony"] {
194                vd.req_field(tier);
195                vd.field_integer(tier);
196            }
197            if block.has_key("ai_check_interval") {
198                let msg = "must not have both `ai_check_interval` and `ai_check_interval_by_tier`";
199                warn(ErrorKey::Validation).msg(msg).loc(key).push();
200            }
201        });
202
203        vd.field_validated_block("ai_target_quick_trigger", |block, data| {
204            let mut vd = Validator::new(block, data);
205            vd.field_bool("adult");
206            vd.field_choice(
207                "rank",
208                &["barony", "county", "duchy", "kingdom", "empire", "hegemony"],
209            );
210            vd.field_list_items("government_type", Item::GovernmentType);
211        });
212
213        vd.field_bool("show_in_list");
214        vd.field_bool("is_important");
215        vd.field_choice(
216            "target_title_tier",
217            &["barony", "county", "duchy", "kingdom", "empire", "hegemony"],
218        );
219
220        // TODO: figure out valid values for this choice type
221        vd.field_value("group");
222
223        vd.field_item("completion_sound_effect", Item::Sound);
224    }
225}
226
227fn validate_contributor_filter(block: &Block, data: &Everything) {
228    let mut vd = Validator::new(block, data);
229    vd.field_bool("vassals");
230    vd.field_bool("tributaries");
231    vd.field_bool("liege");
232    vd.field_bool("top_liege");
233    vd.field_bool("owner");
234    vd.field_bool("allies");
235}
236
237fn validate_contribution(key: &Token, block: &Block, data: &Everything, gp_key: &Token) {
238    let loca = format!("great_project_type_{gp_key}_contribution_{key}");
239    data.verify_exists_implied(Item::Localization, &loca, key);
240    let loca = format!("great_project_type_{gp_key}_contribution_{key}_desc");
241    data.verify_exists_implied(Item::Localization, &loca, key);
242    let loca = format!("great_project_type_{gp_key}_contribution_name_{key}");
243    data.localization.suggest(&loca, key);
244
245    let sc_builder = |key: &Token| {
246        let mut sc = ScopeContext::new(Scopes::Character, key);
247        sc.define_name("province", Scopes::Province, key);
248        sc.define_name("great_project", Scopes::GreatProject, key);
249        sc.define_name("founder", Scopes::Character, key);
250        sc.define_name("owner", Scopes::Character, key);
251        sc
252    };
253
254    let mut vd = Validator::new(block, data);
255    vd.field_trigger_builder("is_shown", Tooltipped::No, sc_builder);
256    vd.field_bool("show_in_planning_phase");
257    // TODO: cryptic doc "Note that we print this in an unevaluated format without scope, so
258    // trigger_ifs are not supported without custom descs"
259    vd.field_trigger_builder("contributor_is_valid", Tooltipped::Yes, sc_builder);
260    vd.field_validated_block("allowed_contributor_filter", validate_contributor_filter);
261    vd.field_trigger_builder("context_allows_contributions", Tooltipped::Yes, sc_builder);
262    vd.field_validated_key_block("cost", |key, block, data| {
263        let mut sc = sc_builder(key);
264        validate_cost(block, data, &mut sc);
265    });
266    vd.field_script_value_builder("contributor_cooldown", sc_builder);
267    vd.field_bool("is_required");
268    vd.field_effect_builder("on_contribution_funded", Tooltipped::Yes, sc_builder);
269    vd.field_effect_builder("on_complete", Tooltipped::Yes, sc_builder);
270    vd.field_script_value_builder("ai_will_do", sc_builder);
271    vd.field_integer("ai_check_interval");
272    vd.field_validated_key_block("ai_check_interval_by_tier", |key, b, data| {
273        let mut vd = Validator::new(b, data);
274        for tier in &["barony", "county", "duchy", "kingdom", "empire", "hegemony"] {
275            vd.req_field(tier);
276            vd.field_integer(tier);
277        }
278        if block.has_key("ai_check_interval") {
279            let msg = "must not have both `ai_check_interval` and `ai_check_interval_by_tier`";
280            warn(ErrorKey::Validation).msg(msg).loc(key).push();
281        }
282    });
283}