1use crate::rng::{derive_stream, Rng};
7use crate::schedule::double_round_robin;
8use fforge_domain::{
9 best_role, Attribute, Attributes, Character, Club, ClubId, Competition, CompetitionId,
10 Fixture, GameDate, Player, PlayerId, Role, Staff, StaffId, StaffRole, World, NUM_ATTRIBUTES,
11 ROLE_WEIGHTS,
12};
13use std::collections::BTreeMap;
14
15pub struct WorldGenConfig {
16 pub num_clubs: usize,
17 pub start_year: i32,
18 pub league_name: String,
19}
20
21impl Default for WorldGenConfig {
22 fn default() -> Self {
23 WorldGenConfig {
24 num_clubs: 20,
25 start_year: 2026,
26 league_name: "Prima Divisione".to_string(),
27 }
28 }
29}
30
31const WORLDGEN_STREAM: u64 = 0x574F_524C_4447_454E; const SQUAD_TEMPLATE: [(Role, usize); 8] = [
35 (Role::Gk, 3),
36 (Role::Cb, 4),
37 (Role::Fb, 4),
38 (Role::Dm, 2),
39 (Role::Cm, 3),
40 (Role::Am, 2),
41 (Role::W, 3),
42 (Role::St, 3),
43];
44
45pub fn generate(seed: u64, cfg: &WorldGenConfig) -> (World, Vec<Fixture>, GameDate) {
46 assert!(cfg.num_clubs >= 2 && cfg.num_clubs % 2 == 0);
47 let mut rng = derive_stream(seed, WORLDGEN_STREAM);
48 let start_date = GameDate::from_year_day(cfg.start_year, 220); let mut qualities: Vec<f64> = (0..cfg.num_clubs)
53 .map(|i| 48.0 + 26.0 * i as f64 / (cfg.num_clubs - 1) as f64)
54 .collect();
55 rng.shuffle(&mut qualities);
56
57 let mut players = BTreeMap::new();
58 let mut clubs = BTreeMap::new();
59 let mut staff = BTreeMap::new();
60 let mut club_ids = Vec::with_capacity(cfg.num_clubs);
61 let mut next_player = 0u32;
62 let mut next_staff = 0u32;
63 let mut used_club_names: Vec<String> = Vec::new();
64
65 for (ci, &quality) in qualities.iter().enumerate() {
66 let club_id = ClubId(ci as u16);
67 club_ids.push(club_id);
68 let name = unique_club_name(&mut rng, &mut used_club_names);
69
70 let mut squad = Vec::new();
71 for &(role, count) in &SQUAD_TEMPLATE {
72 for _ in 0..count {
73 let id = PlayerId(next_player);
74 next_player += 1;
75 let player = gen_player(&mut rng, id, role, quality, start_date);
76 squad.push(id);
77 players.insert(id, player);
78 }
79 }
80
81 let manager_id = StaffId(next_staff);
82 next_staff += 1;
83 staff.insert(
84 manager_id,
85 Staff {
86 id: manager_id,
87 name: person_name(&mut rng),
88 role: StaffRole::Manager,
89 club: Some(club_id),
90 },
91 );
92
93 clubs.insert(
94 club_id,
95 Club {
96 id: club_id,
97 name,
98 players: squad,
99 },
100 );
101 }
102
103 let competition = Competition {
104 id: CompetitionId(0),
105 name: cfg.league_name.clone(),
106 clubs: club_ids.clone(),
107 };
108 let schedule = double_round_robin(&club_ids);
109
110 (
111 World {
112 players,
113 clubs,
114 staff,
115 competition,
116 },
117 schedule,
118 start_date,
119 )
120}
121
122fn gen_player(rng: &mut Rng, id: PlayerId, role: Role, club_quality: f64, today: GameDate) -> Player {
123 let age = 16 + ((rng.f64() + rng.f64()) * 10.0) as i32;
125 let birth = today
126 .add_days(-(age as i64) * fforge_domain::date::DAYS_PER_YEAR)
127 .add_days(-(rng.below(365) as i64));
128
129 let youth_discount = if age < 21 { (21 - age) as f64 * 2.0 } else { 0.0 };
132 let base = rng.normal(club_quality - youth_discount, 5.0).clamp(28.0, 92.0);
133
134 let mut values = [0u8; NUM_ATTRIBUTES];
137 for attr in Attribute::ALL {
138 let w = ROLE_WEIGHTS.weight(role, attr);
139 let v = if w == 0 {
140 rng.range_i32(3, 18) as f64
141 } else {
142 rng.normal(base + (w as f64 - 3.0) * 4.5, 4.5)
143 };
144 values[attr.index()] = v.clamp(1.0, 96.0) as u8;
145 }
146 let attributes = Attributes::new(values);
147
148 let (_, best_ca) = best_role(&attributes, &ROLE_WEIGHTS);
150 let headroom = if age < 24 {
151 (24 - age) * 2 + rng.range_i32(0, 8)
152 } else {
153 rng.range_i32(0, 3)
154 };
155 let potential = (best_ca as i32 + headroom).clamp(best_ca as i32, 97) as u8;
156
157 let character = Character {
158 potential,
159 determination: rng.range_i32(20, 95) as u8,
160 professionalism: rng.range_i32(20, 95) as u8,
161 consistency: rng.range_i32(25, 90) as u8,
162 injury_proneness: rng.range_i32(5, 85) as u8,
163 leadership: rng.range_i32(10, 90) as u8,
164 };
165
166 Player {
167 id,
168 name: person_name(rng),
169 birth,
170 natural_role: role,
171 attributes,
172 character,
173 }
174}
175
176const FIRST_NAMES: &[&str] = &[
179 "Luca", "Marco", "Andrea", "Matteo", "Davide", "Giorgio", "Paolo", "Sandro", "Enzo", "Dario",
180 "Bruno", "Franco", "Nicola", "Sergio", "Tommaso", "Aldo", "Pietro", "Emil", "Jonas", "Karl",
181 "Sven", "Anders", "Milan", "Ivan", "Josip", "Luka", "Marko", "Petar", "Diego", "Rafael",
182 "Thiago", "Bruno", "João", "Pedro", "Nuno", "Rui", "Sami", "Kofi", "Yaya", "Ousmane",
183 "Ibrahim", "Amadou", "Kenji", "Hiro", "Sota", "Owen", "Harry", "Callum",
184];
185
186const LAST_NAMES: &[&str] = &[
187 "Rossi", "Bianchi", "Ferrari", "Colombo", "Ricci", "Marino", "Greco", "Conti", "Gallo",
188 "Fontana", "Moretti", "Barbieri", "Santoro", "Rinaldi", "Vitale", "Longo", "Serra", "Farina",
189 "Berg", "Lund", "Dahl", "Novak", "Horvat", "Kovac", "Babic", "Silva", "Santos", "Costa",
190 "Pereira", "Almeida", "Carvalho", "Mensah", "Diallo", "Traoré", "Keita", "Tanaka", "Sato",
191 "Mori", "Ward", "Hughes", "Walsh", "Kane", "Duarte", "Vega", "Morales", "Iglesias", "Reyes",
192 "Ortega",
193];
194
195fn person_name(rng: &mut Rng) -> String {
196 let first = FIRST_NAMES[rng.below(FIRST_NAMES.len() as u32) as usize];
197 let last = LAST_NAMES[rng.below(LAST_NAMES.len() as u32) as usize];
198 format!("{first} {last}")
199}
200
201const CLUB_PREFIXES: &[&str] = &[
202 "AC", "FC", "US", "Real", "Sporting", "Atlético", "Union", "Inter", "Olimpia", "Racing",
203 "Dinamo", "Virtus",
204];
205
206const CITY_STEMS: &[&str] = &[
207 "Vald", "Mont", "Torr", "Fior", "Cast", "Port", "Riv", "Sald", "Ver", "Bell", "Camp", "Sor",
208 "Lav", "Ner", "Pral", "Ost",
209];
210
211const CITY_ENDS: &[&str] = &[
212 "emona", "averde", "isola", "entino", "ora", "ana", "etto", "iano", "aro", "onte", "urnia",
213 "essa",
214];
215
216fn unique_club_name(rng: &mut Rng, used: &mut Vec<String>) -> String {
217 loop {
218 let prefix = CLUB_PREFIXES[rng.below(CLUB_PREFIXES.len() as u32) as usize];
219 let city = format!(
220 "{}{}",
221 CITY_STEMS[rng.below(CITY_STEMS.len() as u32) as usize],
222 CITY_ENDS[rng.below(CITY_ENDS.len() as u32) as usize]
223 );
224 let name = format!("{prefix} {city}");
225 if !used.contains(&name) {
226 used.push(name.clone());
227 return name;
228 }
229 }
230}