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