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