1 /**
2 PlayerControl.c
3 Functions to handle player controls (i.e. input keys).
4
5 @author Newton
6 */
7
8 static const CON_Gamepad_Deadzone = 60;
9 static CON_VC_Players;
10 static g_player_cursor_pos; // array of [x,y] pos arrays; indexed by player. last cursor pos as sent by CON_CursorPos
11
12 // PlayerControlRelease
13 // Called by engine whenever a control is issued
14 // Forwards control to special handler or cursor
15 // Return whether handled
16 // documented in /docs/sdk/script/fn
PlayerControl(int plr,int ctrl,id spec_id,int x,int y,int strength,bool repeat,int status)17 global func PlayerControl(int plr, int ctrl, id spec_id, int x, int y, int strength, bool repeat, int status)
18 {
19 var release = status == CONS_Up;
20 //Log("%d, %s, %i, %d, %d, %d, %v, %v", plr, GetPlayerControlName(ctrl), spec_id, x,y,strength, repeat, status);
21 // Control handled by definition? Forward
22 if (spec_id) return spec_id->ForwardedPlayerControl(plr, ctrl, x, y, strength, repeat, status);
23
24 // Forward control to player
25 if (Control2Player(plr,ctrl, x, y, strength, repeat, status)) return true;
26
27 // Forward control to cursor
28 var cursor = GetCursor(plr);
29 if (cursor && cursor->GetCrewEnabled())
30 {
31 // Object controlled by plr
32 cursor->SetController(plr);
33
34 // menu controls:
35
36 if (cursor->~GetMenu() && status != CONS_Moved)
37 {
38 // direction keys are always forwarded to the menu
39 // (because the clonk shall not move while the menu is open)
40 var handled = true;
41 if (ctrl == CON_Left) cursor->GetMenu()->~ControlLeft(release);
42 else if (ctrl == CON_Right) cursor->GetMenu()->~ControlRight(release);
43 else if (ctrl == CON_Up) cursor->GetMenu()->~ControlUp(release);
44 else if (ctrl == CON_Down) cursor->GetMenu()->~ControlDown(release);
45 else handled = false;
46 if (handled) return true;
47
48 // cancel menu
49 if (ctrl == CON_CancelMenu)
50 {
51 cursor->TryCancelMenu();
52 return true;
53 }
54
55 if (ctrl == CON_GUIClick1 || ctrl == CON_GUIClick2 || ctrl == CON_GUICursor)
56 {
57 var menux = cursor->GetMenu()->~GetX();
58 var menuy = cursor->GetMenu()->~GetY();
59
60 var dx = x-menux;
61 var dy = y-menuy;
62
63 if (ctrl == CON_GUICursor)
64 {
65 cursor->GetMenu()->~UpdateCursor(dx,dy);
66 return true;
67 }
68 else if (release == true)
69 {
70 cursor->GetMenu()->~OnMouseClick(dx,dy,ctrl == CON_GUIClick2);
71 return false;
72 }
73 }
74 }
75
76 // local coordinates
77 var cursorX = x, cursorY = y;
78 if (x != nil || y != nil)
79 {
80 cursorX -= cursor->GetX();
81 cursorY -= cursor->GetY();
82 }
83
84 // Overload by effect?
85 if (cursor->Control2Effect(plr, ctrl, cursorX, cursorY, strength, repeat, status)) return true;
86
87 if (cursor->ObjectControl(plr, ctrl, cursorX, cursorY, strength, repeat, status))
88 {
89 if (cursor && status == CONS_Down && !repeat)
90 {
91 // non-mouse controls reset view
92 if (!x && !y) ResetCursorView(plr);
93 }
94 return true;
95 }
96 //else Log("-- not handled");
97
98 }
99
100 // Nothing to handle control then
101 return false;
102 }
103
InitializePlayerControl(int plr,string controlset_name,bool keyboard,bool mouse,bool gamepad)104 global func InitializePlayerControl(int plr, string controlset_name, bool keyboard, bool mouse, bool gamepad)
105 {
106 // VC = VirtualCursor
107 if(!CON_VC_Players)
108 CON_VC_Players = CreateArray();
109
110 CON_VC_Players[plr] = !mouse;
111
112 // for all clonks...
113 for (var clonk in FindObjects(Find_OCF(OCF_CrewMember)))
114 {
115 clonk->~ReinitializeControls();
116 }
117 }
118
PlayerHasVirtualCursor(int plr)119 global func PlayerHasVirtualCursor(int plr)
120 {
121 if (!CON_VC_Players)
122 return false;
123
124 return CON_VC_Players[plr];
125 }
126
127 // Control2Player
128 // Player-wide controls
Control2Player(int plr,int ctrl,int x,int y,int strength,bool repeat,int status)129 global func Control2Player(int plr, int ctrl, int x, int y, int strength, bool repeat, int status)
130 {
131 // select previous or next
132 if (ctrl == CON_PreviousCrew)
133 return ShiftCursor(plr, true);
134 if (ctrl == CON_NextCrew)
135 return ShiftCursor(plr, false);
136
137 // all those hotkeys...
138 var hotkey = 0;
139 if (ctrl == CON_PlayerHotkey0) hotkey = 10;
140 if (ctrl == CON_PlayerHotkey1) hotkey = 1;
141 if (ctrl == CON_PlayerHotkey2) hotkey = 2;
142 if (ctrl == CON_PlayerHotkey3) hotkey = 3;
143 if (ctrl == CON_PlayerHotkey4) hotkey = 4;
144 if (ctrl == CON_PlayerHotkey5) hotkey = 5;
145 if (ctrl == CON_PlayerHotkey6) hotkey = 6;
146 if (ctrl == CON_PlayerHotkey7) hotkey = 7;
147 if (ctrl == CON_PlayerHotkey8) hotkey = 8;
148 if (ctrl == CON_PlayerHotkey9) hotkey = 9;
149
150 if (hotkey > 0)
151 {
152 // valid crew number?
153 var crew = GetCrew(plr,hotkey-1);
154 if (!crew) return false;
155 // stop previously selected crew
156 StopSelected();
157
158 // set cursor if not disabled etc.
159 return SetCursor(plr, crew);
160 }
161
162 // Modifier keys - do not handle the key. The GetPlayerControlState will still return the correct value when the key is held down.
163 if (ctrl == CON_ModifierMenu1) return false;
164
165 // cursor pos info - store in player values
166 if (ctrl == CON_CursorPos)
167 {
168 if (!g_player_cursor_pos) g_player_cursor_pos = CreateArray(plr+1);
169 g_player_cursor_pos[plr] = [x, y];
170 return true;
171 }
172 /*
173 if (ctrl == CON_Test)
174 {
175 Message(Format("%d %d (%d %d) %d [%d %d]", plr, ctrl, x, y, strength, repeat, release));
176 return true;
177 }
178 */
179 return false;
180 }
181
182 /* return info of last sent CON_CursorPos packet for that player as [x, y] */
GetPlayerCursorPos(int plr)183 global func GetPlayerCursorPos(int plr)
184 {
185 if (!g_player_cursor_pos) return 0;
186 return g_player_cursor_pos[plr];
187 }
188
StopSelected(int plr)189 global func StopSelected(int plr)
190 {
191 var cursor = GetCursor(plr);
192 if (cursor)
193 {
194 cursor->SetCommand("None");
195 cursor->SetComDir(COMD_Stop);
196 }
197 }
198
199 /* Object functions */
200 // To be called in an object context only!
201
202 // Control2Effect
203 // Call control function in all effects that have "Control" in their name
Control2Effect(int plr,int ctrl,int x,int y,int strength,bool repeat,int status)204 global func Control2Effect(int plr, int ctrl, int x, int y, int strength, bool repeat, int status)
205 {
206 // x and y are local coordinates
207 if (!this) return false;
208
209 // Count down from EffectCount, in case effects get deleted
210 var i = GetEffectCount("*Control*", this), fx;
211 while (i--)
212 {
213 fx = GetEffect("*Control*", this, i);
214 if (fx)
215 if (EffectCall(this, fx, "Control", ctrl, x,y,strength, repeat, status))
216 return true;
217 }
218 // No effect handled the control
219 return false;
220 }
221
222 // ObjectControl
223 // Called from PlayerControl when a control is issued to the cursor
224 // Return whether handled
225 // To be overloaded by specific objects to enable additional controls
ObjectControl(int plr,int ctrl,int x,int y,int strength,bool repeat,int status)226 global func ObjectControl(int plr, int ctrl, int x, int y, int strength, bool repeat, int status)
227 {
228 if (!this) return false;
229
230 // Any control resets a previously given command
231 SetCommand("None");
232
233 // Movement controls
234 if (ctrl == CON_Left || ctrl == CON_Right || ctrl == CON_Up || ctrl == CON_Down || ctrl == CON_Jump)
235 return ObjectControlMovement(plr, ctrl, strength, status, repeat);
236
237 // Unhandled control
238 return false;
239 }
240
241 // Find an object with an entrance in front of this object whose entrance is at
242 // the right position
GetEntranceObject()243 global func GetEntranceObject()
244 {
245 if (!this) return nil;
246
247 // object with an entrance on target position
248 var obj = FindObject(Find_OCF(OCF_Entrance), Find_Layer(GetObjectLayer()),
249 Find_AtPoint(0,0), Find_Exclude(this));
250 if (!obj) return nil;
251
252 var x = obj->GetDefCoreVal("Entrance","DefCore",0) + obj->GetX();
253 var y = obj->GetDefCoreVal("Entrance","DefCore",1) + obj->GetY();
254 var wdt = obj->GetDefCoreVal("Entrance","DefCore",2);
255 var hgt = obj->GetDefCoreVal("Entrance","DefCore",3);
256
257 // entrance is on the vehicle?
258 if (!Inside(GetX(), x, x+wdt)) return nil;
259 if (!Inside(GetY(), y, y+hgt)) return nil;
260
261 return obj;
262 }
NameComDir(comdir)263 global func NameComDir(comdir)
264 {
265 if(comdir == COMD_Stop) return "COMD_Stop";
266 if(comdir == COMD_Up) return "COMD_Up";
267 if(comdir == COMD_UpRight) return "COMD_UpRight";
268 if(comdir == COMD_UpLeft) return "COMD_UpLeft";
269 if(comdir == COMD_Right) return "COMD_Right";
270 if(comdir == COMD_Left) return "COMD_Left";
271 if(comdir == COMD_Down) return "COMD_Down";
272 if(comdir == COMD_DownRight) return "COMD_DownRight";
273 if(comdir == COMD_DownLeft) return "COMD_DownLeft";
274 if(comdir == COMD_None) return "COMD_None";
275 }
276 // Called when CON_Left/Right/Up/Down controls are issued/released
277 // Return whether handled
ObjectControlMovement(int plr,int ctrl,int strength,int status,bool repeat)278 global func ObjectControlMovement(int plr, int ctrl, int strength, int status, bool repeat)
279 {
280 if (!this) return false;
281
282 // movement is only possible when not contained
283 if (Contained()) return false;
284
285 // this is for controlling movement with Analogpad
286 if (status == CONS_Down)
287 if (strength != nil && strength < CON_Gamepad_Deadzone)
288 return true;
289
290 var proc = GetProcedure();
291 // Some specific movement controls
292 if (status == CONS_Down)
293 {
294 // Jump control
295 if (ctrl == CON_Jump)
296 {
297 if (proc == "WALK" && GetComDir() == COMD_Up)
298 SetComDir(COMD_Stop);
299 if (proc == "WALK")
300 {
301 this->ObjectCommand("Jump");
302 return true;
303 }
304 }
305 if (proc == "SWIM" && !GBackSemiSolid(0,-5)) //Water jump
306 {
307 if (ctrl == CON_Up) return false;
308 else if(ctrl == CON_Jump) this->ObjectCommand("Jump");
309 }
310 if (proc == "SCALE") // Let go from scaling a wall
311 {
312 //Wall kick
313 if (ctrl == CON_Left && GetDir() == DIR_Right && GetComDir() == COMD_Up)
314 return this->ObjectCommand("Jump");
315 if (ctrl == CON_Right && GetDir() == DIR_Left && GetComDir() == COMD_Up)
316 return this->ObjectCommand("Jump");
317
318 //Let go of wall
319 if (ctrl == CON_Left && GetDir() == DIR_Right) return this->ObjectComLetGo(-10);
320 if (ctrl == CON_Right && GetDir() == DIR_Left) return this->ObjectComLetGo(+10);
321 }
322 else if (proc == "HANGLE") // Let go from hangling the ceiling
323 {
324 if (ctrl == CON_Down) return this->ObjectComLetGo(0,0);
325 }
326 // Direct turnaround if object is standing still. Valid for any procedure in OC
327 if (!GetXDir())
328 {
329 if (ctrl == CON_Left) SetDir(DIR_Left);
330 else if (ctrl == CON_Right) SetDir(DIR_Right);
331 }
332 }
333 else // release
334 {
335 // If rolling, allow to instantly switch to walking again.
336 if (GetAction() == "Roll")
337 {
338 if (ctrl == CON_Left && GetDir() == DIR_Left || ctrl == CON_Right && GetDir() == DIR_Right)
339 SetAction("Walk");
340 }
341 }
342 return ObjectControlUpdateComdir(plr);
343 }
344
345 // Updates ComDir of object based on current Con_*-directional controls
346 // Return whether actual, effective direction of movement changed
ObjectControlUpdateComdir(int plr)347 global func ObjectControlUpdateComdir(int plr)
348 {
349 if (!this) return false;
350
351 // Generic movement: Update ComDir based on current control state
352 var new_comdir = GetPlayerConDir(plr, CON_Left, CON_Up, CON_Right, CON_Down);
353 var old_comdir = GetComDir();
354 if (new_comdir != old_comdir)
355 {
356 // ComDir changed. Update.
357 SetComDir(new_comdir);
358 //var s = "";
359 //if (GetPlayerControlState(plr, CON_Left)) s = Format("%sL", s);
360 //if (GetPlayerControlState(plr, CON_Up)) s = Format("%sU", s);
361 //if (GetPlayerControlState(plr, CON_Right)) s = Format("%sR", s);
362 //if (GetPlayerControlState(plr, CON_Down)) s = Format("%sD", s);
363 //s = Format("%s %s", s, ["Stop", "Up", "UpRight", "Right", "DownRight", "Down", "DownLeft", "Left", "UpLeft"][new_comdir]);
364 //Message("@%s", this, s);
365 // The control is only handled if it had an actual effect on the current movement direction of the Clonk
366 var old = ComDir2XY(old_comdir);
367 var new = ComDir2XY(new_comdir);
368 var old_cx = old[0], old_cy = old[1], new_cx = new[0], new_cy = new[1];
369 var is_handled;
370 var proc = GetProcedure();
371 if (proc == "WALK" || proc == "HANGLE" || proc == "PUSH" || proc == "PULL" || proc == "FLIGHT")
372 // Only horizontal movement changed actual direction
373 // Also, enforce clear Left/Right commands without leftover Up/Down
374 // CON_Down is never handled this way, thus forcing a CON_Stop
375 is_handled = (old_cx != new_cx) && !new_cy;
376 else if (proc == "SCALE")
377 // Only vertical movement changed actual direction
378 // Also, enfore clear Up/Down to prevent "Zuppel" in corner
379 is_handled = (old_cy != new_cy) && !new_cx;
380 else if (proc == "SWIM" || proc == "FLOAT" || proc == "DIG")
381 is_handled = (old_cx != new_cx || old_cy != new_cy); // Free 360 degree movement
382 else
383 is_handled = false;
384 return is_handled;
385 }
386 else
387 {
388 // ComDir did not change. -> The control was not handled
389 //Log("NoChange");
390 return false;
391 }
392 }
393
394 // selects the next/previous crew member (that is not disabled)
ShiftCursor(int plr,bool back,bool force)395 global func ShiftCursor(int plr, bool back, bool force)
396 {
397 // Is the selected Clonk busy at the moment? E.g. uncloseable menu open..
398 if (!force)
399 {
400 var cursor = GetCursor(plr);
401 if (cursor && cursor->~RejectShiftCursor()) return false;
402 }
403
404 // get index of currently selected crew
405 var index = 0;
406 while (index < GetCrewCount(plr))
407 {
408 if (GetCursor(plr) == GetCrew(plr,index)) break;
409 index++;
410 }
411
412 // a short explanation here:
413 // when shifting the cursor to the next crew member, this crew member
414 // might be disabled via SetCrewEnabled. That is why we have to go to
415 // the crew after the next and perhaps even the crew after the next
416 // after the next. Also, we need to stop this skipping after all crew
417 // members have been checked in the special case that all crew members
418 // are disabled
419 var maxcycle = GetCrewCount(plr);
420 var cycle = 0;
421
422 do {
423 if (back)
424 {
425 --index;
426 if (index < 0) index = GetCrewCount(plr)-1;
427 }
428 else
429 {
430 ++index;
431 if (index >= GetCrewCount(plr)) index = 0;
432 }
433 ++cycle;
434 } while (cycle < maxcycle && !(GetCrew(plr,index)->GetCrewEnabled()));
435
436 // Changing the cursor closes all menus that are associated with the old cursor.
437 // However, if a menu is not closable, then it requires the attention of the player and switching the cursor is disabled..
438 var current_cursor = GetCursor(plr);
439 var new_cursor = GetCrew(plr, index);
440 if (current_cursor == new_cursor) return false;
441
442 StopSelected(plr);
443 if (current_cursor)
444 current_cursor->~OnShiftCursor(new_cursor);
445
446 return SetCursor(plr, new_cursor);
447 }
448
449 // Temporarily used for Debugging!
450 // Helper function to turn CON_*-constants into strings
GetPlayerControlName(int ctrl)451 global func GetPlayerControlName(int ctrl)
452 {
453 var con_name = GetConstantNameByValue(ctrl, "CON_");
454 if (!con_name) con_name = Format("Unknown(%d)", ctrl);
455 return con_name;
456 }
457
458 // Return COMD_*-constant corresponding to current state of passed directional controls
GetPlayerConDir(int plr,int con_left,int con_up,int con_right,int con_down)459 global func GetPlayerConDir(int plr, int con_left, int con_up, int con_right, int con_down)
460 {
461 var x,y;
462 if (GetPlayerControlState(plr, con_left)) --x;
463 if (GetPlayerControlState(plr, con_up)) --y;
464 if (GetPlayerControlState(plr, con_right)) ++x;
465 if (GetPlayerControlState(plr, con_down)) ++y;
466 // Creating an array here for every keypress/release
467 // Would be so cool to have this static const. Guenther?
468 var dir_coms = [COMD_UpLeft, COMD_Up, COMD_UpRight, COMD_Left, COMD_Stop, COMD_Right, COMD_DownLeft, COMD_Down, COMD_DownRight];
469 return dir_coms[y*3+x+4];
470 }
471
472 // Returns coordinate directions associated with a COMD_Constant
ComDir2XY(int comd)473 global func ComDir2XY(int comd)
474 {
475 // Creating an array here for every keypress/release
476 // Would be so cool to have this static const. Guenther?
477 return [[0,0,1,1,1,0,-1,-1,-1][comd], [0,-1,-1,0,1,1,1,0,-1][comd]];
478 }
479
ObjectCommand(string command,object target,int tx,int ty,object target2,data)480 global func ObjectCommand(string command, object target, int tx, int ty, object target2, /*any*/ data)
481 {
482 // this function exists to be overloadable by ClonkControl.c4d
483 if (!this)
484 return;
485 this->SetCommand(command, target, tx, ty, target2, data);
486 }
487
488 // Let go from scaling or hangling
ObjectComLetGo(int vx,int vy)489 global func ObjectComLetGo(int vx, int vy)
490 {
491 if (!SetAction("Jump")) return false;
492 SetXDir(vx); SetYDir(vy);
493 return true;
494 }
495
496 /* Mouse Hovering */
497
498 // Engine callback when the mouse hovers over an object (entering) or stops hovering over an object (leaving).
499 // Either leaving, entering or both are set. They can be nil, but never both at the same time.
500 // dragged is an object being drag(¬ dropped yet)
MouseHover(int player,object leaving,object entering,object dragged)501 global func MouseHover(int player, object leaving, object entering, object dragged)
502 {
503 // Leaving the hovering zone should be processed first.
504 if (leaving)
505 leaving->~OnMouseOut(player, dragged);
506 // Then process entering a new hovering zone.
507 if (entering)
508 entering->~OnMouseOver(player, dragged);
509 return true;
510 }
511
512 /* Drag & Drop */
513
514 // Engine callback on drag&drop: gives the player, the dragged obect and the object which is being dropped(can be nil).
MouseDragDrop(int plr,object source,object target)515 global func MouseDragDrop(int plr, object source, object target)
516 {
517 //Log("MouseDragDrop(%d, %s, %s)", plr, source->GetName(), target->GetName());
518 if (!source) return false; // can happen if source got deleted after control was queued
519 var src_drag = source->~OnMouseDrag(plr);
520 if (!src_drag)
521 return false;
522
523 // If there is drop target, check whether it accepts the drop.
524 if (target)
525 if (!target->~OnMouseDrop(plr, src_drag))
526 return false;
527
528 // Notify the drop object it succeeded.
529 if (source)
530 source->~OnMouseDragDone(src_drag, target);
531 return true;
532 }
533
534 /* Debug */
535
536 // uncomment this to get log messages for any player control issued
537 /*global func PlayerControl(int plr, int ctrl, id spec_id, int x, int y, int strength, bool repeat, bool release)
538 {
539 var r = inherited(plr, ctrl, spec_id, x, y, strength, repeat, release, ...), rs;
540 if (r) rs = ""; else rs = "!";
541 Log("%s%d, %s, %i, %d, %d, %d, %v, %v", rs, plr, GetPlayerControlName(ctrl), spec_id, x,y,strength, repeat, release);
542 return r;
543 }*/
544
545 /*
546 This is used by Library_ClonkInventoryControl and needs to be a global function (in a non-appendto).
547 This function returns the priority of an object when selecting an object to pick up.
548 */
Library_ClonkInventoryControl_Sort_Priority(int x_position)549 global func Library_ClonkInventoryControl_Sort_Priority(int x_position)
550 {
551 // Objects are sorted by position, preferring the direction of the key press.
552 var priority_x = GetX() - x_position;
553 if (priority_x < 0) priority_x += 1000;
554 return priority_x;
555 }
556