1 /**
2 Player start
3 Controls start position and modalities
4
5 @author Sven2
6 */
7
8
9 /* Definition */
10
11 local starting_players = { Option="all" };
12 local starting_knowledge = { Option="all" };
13 local starting_crew; // const arrays not supported yet
14 local starting_material;
15 local starting_wealth = 0;
16 local starting_base_material;
17 local respawn_material;
18 local clonk_max_contents_count, clonk_max_energy; // Override properties for clonks
19 local view_lock = false;
20 local zoom_min, zoom_max, zoom_set;
21 local Name = "$Name$";
22 local Description = "$Description$";
23 local Visibility = VIS_Editor;
24 local Plane = 311;
25 local players_started; // Array of players for which this was the start point
26
Definition(def)27 public func Definition(def)
28 {
29 def.starting_crew = GetDefaultCrew();
30 def.starting_material = GetDefaultMaterial();
31 def.starting_base_material = GetDefaultBaseMaterial();
32 if (!def.EditorProps) def.EditorProps = {};
33 def.EditorProps.starting_players = EditorBase.PlayerMask;
34 def.EditorProps.starting_knowledge = { Name="$Knowledge$", Type="enum", OptionKey="Option", Options = [
35 { Name="$None$" },
36 { Name="$All$", Value={ Option="all" } },
37 { Name="$AllExcept$", Value={ Option="allexcept", Data=[] }, ValueKey="Data", Delegate=EditorBase.IDSet },
38 { Name="$Specific$", Value={ Option="idlist", Data=[] }, ValueKey="Data", Delegate=EditorBase.IDSet },
39 ] };
40 def.EditorProps.starting_crew = EditorBase->GetConditionalIDList("IsClonk", "$Crew$", Clonk);
41 def.EditorProps.starting_material = new EditorBase.ItemPlusParameterList { Name="$StartingMaterial$", EditorHelp="$StartingMaterialHelp$" };
42 def.EditorProps.starting_wealth = { Name="$Wealth$", Type="int", Min=0 };
43 def.EditorProps.starting_base_material = new EditorBase.IDList { Name="$BaseMaterial$", EditorHelp="$BaseMaterialHelp$" };
44 def.EditorProps.clonk_max_contents_count = { Name="$ClonkMaxContentsCount$", EditorHelp="$ClonkMaxContentsCountHelp$", Type="enum", Options = [
45 { Name=Format("$Default$ (%d)", Clonk.MaxContentsCount) }, { Name="$Custom$", Value=Clonk.MaxContentsCount, Delegate={ Type="int", Min=0, Max=10 } } ] };
46 def.EditorProps.clonk_max_energy = { Name="$ClonkMaxEnergy$", EditorHelp="$ClonkMaxEnergyHelp$", Type="enum", Options = [
47 { Name=Format("$Default$ (%d)", Clonk.MaxEnergy/1000) }, { Name="$Custom$", Value=Clonk.MaxEnergy/1000, Delegate={ Type="int", Min=1, Max=100000 } } ] };
48 def.EditorProps.respawn_material = { Name="$RespawnMaterial$", Type="enum", Set="SetRespawnMaterial", Save="RespawnMaterial", Options = [
49 { Name="$None$" },
50 { Name="$SameAsStartingMaterial$", Value="starting_material" },
51 { Name="$Custom$", Value=[], Type=C4V_Array, Delegate=new EditorBase.ItemPlusParameterList { Name="$RespawnMaterial$", EditorHelp="$RespawnMaterialHelp$" } },
52 ] };
53 def.EditorProps.view_lock = { Name="$ViewLock$", Priority = -100, Type="bool" };
54 def.EditorProps.zoom_min = { Name="$ZoomMin$", Set="SetZoomMin", Priority = -101, Type="enum", OptionKey="Option", Options = [
55 { Name="$Default$" },
56 { Name="$Custom$", Value=150, Delegate={ Type="int", Min=50, Max=750, Step=50 } }
57 ] };
58 def.EditorProps.zoom_max = { Name="$ZoomMax$", Set="SetZoomMax", Priority = -102, Type="enum", OptionKey="Option", Options = [
59 { Name="$Default$" },
60 { Name="$Custom$", Value=750, Delegate={ Type="int", Min=150, Max=100000, Step=50 } }
61 ] };
62 def.EditorProps.zoom_set = { Name="$ZoomSet$", Set="SetZoomSet", Priority = -103, Type="enum", OptionKey="Option", Options = [
63 { Name="$Default$" },
64 { Name="$Custom$", Value=300, Delegate={ Type="int", Min=150, Max=750, Step=50 } }
65 ] };
66 return true;
67 }
68
GetDefaultCrew()69 public func GetDefaultCrew() { return [{id=Clonk, count=1}]; }
GetDefaultMaterial()70 public func GetDefaultMaterial() { return [Shovel, Hammer, Axe]; }
GetDefaultBaseMaterial()71 public func GetDefaultBaseMaterial() { return [{id=Clonk, count=999999}]; }
72
Initialize()73 public func Initialize()
74 {
75 // Re-init default
76 starting_crew = GetDefaultCrew();
77 starting_material = GetDefaultMaterial();
78 starting_base_material = GetDefaultBaseMaterial();
79 return true;
80 }
81
82
83 /* Interface */
84
SetStartingPlayers(string setting,param)85 public func SetStartingPlayers(string setting, param)
86 {
87 if (setting)
88 starting_players = { Option=setting, Data=param };
89 else
90 starting_players = nil; // None
91 return true;
92 }
93
SetStartingKnowledge(string setting,param)94 public func SetStartingKnowledge(string setting, param)
95 {
96 if (setting)
97 starting_knowledge = { Option=setting, Data=param };
98 else
99 starting_knowledge = nil; // None
100 return true;
101 }
102
SetStartingCrew(array new_crew)103 public func SetStartingCrew(array new_crew)
104 {
105 starting_crew = new_crew;
106 return true;
107 }
108
SetStartingMaterial(array new_material)109 public func SetStartingMaterial(array new_material)
110 {
111 // ID+count conversion (old style)
112 if (new_material && GetLength(new_material) && new_material[0].id && new_material[0].count && !new_material[0]->~GetName())
113 {
114 starting_material = [];
115 var n = 0;
116 for (var idlist_entry in new_material)
117 for (var i = 0; i < idlist_entry.count; ++i)
118 starting_material[n++] = idlist_entry.id;
119 }
120 else
121 {
122 starting_material = new_material;
123 }
124 return true;
125 }
126
SetStartingWealth(int new_wealth)127 public func SetStartingWealth(int new_wealth)
128 {
129 starting_wealth = new_wealth;
130 return true;
131 }
132
SetStartingBaseMaterial(array new_material)133 public func SetStartingBaseMaterial(array new_material)
134 {
135 starting_base_material = new_material;
136 return true;
137 }
138
SetClonkMaxContentsCount(int new_clonk_max_contents_count)139 public func SetClonkMaxContentsCount(int new_clonk_max_contents_count)
140 {
141 clonk_max_contents_count = new_clonk_max_contents_count;
142 return true;
143 }
144
SetClonkMaxEnergy(int new_clonk_max_energy)145 public func SetClonkMaxEnergy(int new_clonk_max_energy)
146 {
147 clonk_max_energy = new_clonk_max_energy;
148 return true;
149 }
150
SetRespawnMaterial(new_material)151 public func SetRespawnMaterial(new_material)
152 {
153 respawn_material = new_material;
154 }
155
SetViewLock(bool lock)156 public func SetViewLock(bool lock)
157 {
158 view_lock = lock;
159 }
160
SetZoomMin(int zoom)161 public func SetZoomMin(int zoom)
162 {
163 zoom_min = zoom;
164 this.EditorProps.zoom_max.Options[1].Delegate.Min = zoom_min;
165 this.EditorProps.zoom_set.Options[1].Delegate.Min = zoom_min;
166 SetZoomSet(Max(zoom_set ?? this.EditorProps.zoom_set.Options[1].Value, zoom_min));
167 }
168
SetZoomMax(int zoom)169 public func SetZoomMax(int zoom)
170 {
171 zoom_max = zoom;
172 this.EditorProps.zoom_max.Options[1].Delegate.Max = zoom_max;
173 this.EditorProps.zoom_set.Options[1].Delegate.Max = zoom_max;
174 SetZoomSet(Min(zoom_set ?? this.EditorProps.zoom_set.Options[1].Value, zoom_max));
175 }
176
SetZoomSet(int zoom)177 public func SetZoomSet(int zoom)
178 {
179 zoom_set = zoom;
180 for (var plr in GetPlayers(C4PT_User))
181 InitializeView(plr);
182 }
183
184
185 /* Player initialization checks */
186
InitializePlayer(int plr,x,y,base,team,script_id)187 public func InitializePlayer(int plr, x, y, base, team, script_id)
188 {
189 // Find which one to evaluate
190 var possible_startpoints = FindObjects(Find_ID(PlayerStart), Find_Func("IsStartFor", plr));
191 var n = GetLength(possible_startpoints);
192 if (!n) return false;
193 // This callback will be done for every start point (unfortunately)
194 // So ensure initialization happens only once
195 // (Could speed up things by setting a variable in the other start points to avoid the redundant search. Meh it's just initialization anyway.)
196 // Note that this method assumes that starting points are returned in a predictable order
197 if (this != possible_startpoints[0]) return false;
198 // Pick best starting point: Away from other players, especially enemies
199 for (var startpoint in possible_startpoints)
200 {
201 var other_clonks = startpoint->FindObjects(Find_Distance(50), Find_OCF(OCF_CrewMember));
202 var hostile = 0;
203 for (var c in other_clonks) if (Hostile(c->GetOwner(), plr)) ++hostile;
204 startpoint.penalty = GetLength(other_clonks) + hostile*1000;
205 }
206 SortArrayByProperty(possible_startpoints, "penalty");
207 var n_best = 1, best_penalty = possible_startpoints[0].penalty;
208 if (n>1) while (possible_startpoints[n_best].penalty == best_penalty) if (++n_best == n) break;
209 // Launch there
210 possible_startpoints[Random(n_best)]->DoPlayerStart(plr);
211 return true;
212 }
213
IsStartFor(int plr)214 public func IsStartFor(int plr)
215 {
216 return EditorBase->EvaluatePlayerMask(starting_players , plr);
217 }
218
219
220 /* Actual player initialization */
221
222 local is_handling_player_spawn; // temp var set to nonzero during initial player spawn (to differentiate from respawn)
223
DoPlayerStart(int plr)224 public func DoPlayerStart(int plr)
225 {
226 // Player launch controlled by this object!
227 if (!players_started) players_started = [];
228 players_started[GetLength(players_started)] = plr;
229 ++is_handling_player_spawn;
230 // Give wealth
231 SetWealth(plr, starting_wealth);
232 // Set base material
233 InitializeBaseMaterial(plr);
234 // Create requested crew
235 InitializeCrew(plr);
236 // Put contents into crew
237 InitializeMaterial(plr);
238 // Give knowledge
239 InitializeKnowledge(plr);
240 // Handle viewport settings
241 InitializeView(plr);
242 --is_handling_player_spawn;
243 return true;
244 }
245
RemovePlayer(int plr)246 public func RemovePlayer(int plr)
247 {
248 // Remove number from players_started list
249 if (players_started)
250 {
251 var idx = GetIndexOf(players_started, plr);
252 if (idx >= 0)
253 {
254 var n = GetLength(players_started) - 1;
255 players_started[idx] = players_started[n];
256 SetLength(players_started, n);
257 }
258 }
259 }
260
OnClonkRecruitment(clonk,plr)261 public func OnClonkRecruitment(clonk, plr)
262 {
263 // New clonk recruitment: Apply default clonk settings
264 if (players_started && GetIndexOf(players_started, plr) >= 0)
265 {
266 ApplyCrewSettings(clonk);
267 if (!is_handling_player_spawn && respawn_material)
268 {
269 if (respawn_material == "starting_material")
270 {
271 // Same as startign material
272 InitializeMaterial(plr);
273 }
274 else
275 {
276 // Array of custom respawn material
277 for (var idlist_entry in respawn_material)
278 clonk->CreateContents(idlist_entry.id, idlist_entry.count);
279 }
280 }
281 }
282 }
283
ApplyCrewSettings(object crew)284 private func ApplyCrewSettings(object crew)
285 {
286 if (GetType(clonk_max_contents_count)) crew->~SetMaxContentsCount(clonk_max_contents_count);
287 if (GetType(clonk_max_energy))
288 {
289 crew->~SetMaxEnergy(clonk_max_energy*1000);
290 crew->DoEnergy(clonk_max_energy);
291 }
292 return true;
293 }
294
InitializeCrew(int plr)295 private func InitializeCrew(int plr)
296 {
297 // Collect IDs of crew to create
298 var requested_crew = [], n=0, i, obj, idx, def;
299 for (var idlist_entry in starting_crew)
300 for (i=0; i<idlist_entry.count; ++i)
301 requested_crew[n++] = idlist_entry.id;
302 // Match them to existing crew
303 for (i = GetCrewCount(plr)-1; i>=0; --i)
304 if (obj = GetCrew(plr, i))
305 if ((idx = GetIndexOf(requested_crew, obj->GetID())) >= 0)
306 {
307 obj->SetPosition(GetX(), GetY() + GetDefHeight()/2 - obj->GetDefHeight()/2);
308 requested_crew[idx] = nil;
309 }
310 else
311 obj->RemoveObject(); // not in list: Kill
312 // Create any missing crew
313 for (def in requested_crew)
314 if (def)
315 if (obj = CreateObjectAbove(def, 0, GetDefHeight()/2, plr))
316 obj->MakeCrewMember(plr);
317 // Apply crew settings
318 for (i = GetCrewCount(plr)-1; i>=0; --i)
319 if (obj = GetCrew(plr, i))
320 ApplyCrewSettings(obj);
321 // Done!
322 return true;
323 }
324
InitializeBaseMaterial(int plr)325 private func InitializeBaseMaterial(int plr)
326 {
327 // Set base material to minimum of current material and material given by this object
328 if (starting_base_material)
329 {
330 for (var entry in starting_base_material)
331 {
332 var current_num = GetBaseMaterial(plr, entry.id);
333 if (current_num < entry.count)
334 {
335 SetBaseMaterial(plr, entry.id, entry.count);
336 }
337 }
338 }
339 return true;
340 }
341
InitializeMaterial(int plr)342 private func InitializeMaterial(int plr)
343 {
344 // Spread material across clonks. Try to fill them evenly and avoid giving the same item twice to the same clonk
345 // So e.g. each clonk can get one shovel
346 for (var idlist_entry in starting_material)
347 {
348 var best_target = nil, target_score, clonk;
349 var obj = EditorBase->CreateItemPlusParameter(idlist_entry, GetX(),GetY()+GetDefHeight()/2, plr);
350 if (!obj || !obj.Collectible) continue;
351 var id = idlist_entry.id;
352 for (var j=0; j<GetCrewCount(plr); ++j)
353 if (clonk = GetCrew(plr, j))
354 {
355 var clonk_score = 0;
356 // High penalty: Already has item of same type
357 clonk_score += clonk->ContentsCount(id)*1000;
358 // Low penalty: Already has items
359 clonk_score += clonk->ContentsCount();
360 if (!best_target || clonk_score < target_score)
361 {
362 best_target = clonk;
363 target_score = clonk_score;
364 }
365 }
366 if (best_target) best_target->Collect(obj); // May fail due to contents full
367 }
368 return true;
369 }
370
InitializeKnowledge(int plr)371 private func InitializeKnowledge(int plr)
372 {
373 var def;
374 if (!starting_knowledge) return true; // No knowledge
375 if (starting_knowledge.Option == "all" || starting_knowledge.Option == "allexcept")
376 {
377 var i=0, exceptlist = [];
378 if (starting_knowledge.Option == "allexcept") exceptlist = starting_knowledge.Data;
379 while (def = GetDefinition(i++))
380 if (!(def->GetCategory() & (C4D_Rule | C4D_Goal | C4D_Environment)))
381 if (GetIndexOf(exceptlist, def) == -1)
382 SetPlrKnowledge(plr, def);
383 }
384 else if (starting_knowledge.Option == "idlist")
385 {
386 for (def in starting_knowledge.Data)
387 SetPlrKnowledge(plr, def);
388 }
389 else
390 {
391 // Unknown option
392 return false;
393 }
394 return true;
395 }
396
InitializeView(int plr)397 private func InitializeView(int plr)
398 {
399 SetPlayerViewLock(plr, view_lock);
400 SetPlayerZoomByViewRange(plr, zoom_min, PLRZOOM_Direct | PLRZOOM_LimitMin);
401 SetPlayerZoomByViewRange(plr, zoom_max, PLRZOOM_Direct | PLRZOOM_LimitMax);
402 SetPlayerZoomByViewRange(plr, zoom_set, PLRZOOM_Direct | PLRZOOM_Set);
403 return true;
404 }
405
406
407 /* Scenario saving */
408
SaveScenarioObject(props,...)409 public func SaveScenarioObject(props, ...)
410 {
411 if (!inherited(props, ...)) return false;
412 if (!DeepEqual(starting_players, GetID().starting_players))
413 if (starting_players)
414 props->AddCall("Players", this, "SetStartingPlayers", Format("%v", starting_players.Option), starting_players.Data);
415 else
416 props->AddCall("Players", this, "SetStartingPlayers", nil);
417 if (!DeepEqual(starting_knowledge, GetID().starting_knowledge))
418 if (starting_knowledge)
419 props->AddCall("Knowledge", this, "SetStartingKnowledge", Format("%v", starting_knowledge.Option), starting_knowledge.Data);
420 else
421 props->AddCall("Knowledge", this, "SetStartingKnowledge", nil);
422 if (!DeepEqual(starting_crew, GetID().starting_crew)) props->AddCall("Crew", this, "SetStartingCrew", starting_crew);
423 if (!DeepEqual(starting_material, GetID().starting_material)) props->AddCall("Material", this, "SetStartingMaterial", starting_material);
424 if (!DeepEqual(starting_base_material, GetID().starting_base_material)) props->AddCall("Material", this, "SetStartingBaseMaterial", starting_base_material);
425 if (starting_wealth != GetID().starting_wealth) props->AddCall("Wealth", this, "SetStartingWealth", starting_wealth);
426 if (GetType(clonk_max_contents_count)) props->AddCall("ClonkMaxContentsCount", this, "SetClonkMaxContentsCount", clonk_max_contents_count);
427 if (GetType(clonk_max_energy)) props->AddCall("ClonkMaxEnergy", this, "SetClonkMaxEnergy", clonk_max_energy);
428 if (view_lock != nil) props->AddCall("ViewLock", this, "SetViewLock", view_lock);
429 if (zoom_min != nil) props->AddCall("ZoomMin", this, "SetZoomMin", zoom_min);
430 if (zoom_max != nil) props->AddCall("ZoomMax", this, "SetZoomMax", zoom_max);
431 if (zoom_set != nil) props->AddCall("ZoomSet", this, "SetZoomSet", zoom_set);
432 return true;
433 }
434