/** PlayerControl.c Functions to handle player controls (i.e. input keys). @author Newton */ static const CON_Gamepad_Deadzone = 60; static CON_VC_Players; static g_player_cursor_pos; // array of [x,y] pos arrays; indexed by player. last cursor pos as sent by CON_CursorPos // PlayerControlRelease // Called by engine whenever a control is issued // Forwards control to special handler or cursor // Return whether handled // documented in /docs/sdk/script/fn global func PlayerControl(int plr, int ctrl, id spec_id, int x, int y, int strength, bool repeat, int status) { var release = status == CONS_Up; //Log("%d, %s, %i, %d, %d, %d, %v, %v", plr, GetPlayerControlName(ctrl), spec_id, x,y,strength, repeat, status); // Control handled by definition? Forward if (spec_id) return spec_id->ForwardedPlayerControl(plr, ctrl, x, y, strength, repeat, status); // Forward control to player if (Control2Player(plr,ctrl, x, y, strength, repeat, status)) return true; // Forward control to cursor var cursor = GetCursor(plr); if (cursor && cursor->GetCrewEnabled()) { // Object controlled by plr cursor->SetController(plr); // menu controls: if (cursor->~GetMenu() && status != CONS_Moved) { // direction keys are always forwarded to the menu // (because the clonk shall not move while the menu is open) var handled = true; if (ctrl == CON_Left) cursor->GetMenu()->~ControlLeft(release); else if (ctrl == CON_Right) cursor->GetMenu()->~ControlRight(release); else if (ctrl == CON_Up) cursor->GetMenu()->~ControlUp(release); else if (ctrl == CON_Down) cursor->GetMenu()->~ControlDown(release); else handled = false; if (handled) return true; // cancel menu if (ctrl == CON_CancelMenu) { cursor->TryCancelMenu(); return true; } if (ctrl == CON_GUIClick1 || ctrl == CON_GUIClick2 || ctrl == CON_GUICursor) { var menux = cursor->GetMenu()->~GetX(); var menuy = cursor->GetMenu()->~GetY(); var dx = x-menux; var dy = y-menuy; if (ctrl == CON_GUICursor) { cursor->GetMenu()->~UpdateCursor(dx,dy); return true; } else if (release == true) { cursor->GetMenu()->~OnMouseClick(dx,dy,ctrl == CON_GUIClick2); return false; } } } // local coordinates var cursorX = x, cursorY = y; if (x != nil || y != nil) { cursorX -= cursor->GetX(); cursorY -= cursor->GetY(); } // Overload by effect? if (cursor->Control2Effect(plr, ctrl, cursorX, cursorY, strength, repeat, status)) return true; if (cursor->ObjectControl(plr, ctrl, cursorX, cursorY, strength, repeat, status)) { if (cursor && status == CONS_Down && !repeat) { // non-mouse controls reset view if (!x && !y) ResetCursorView(plr); } return true; } //else Log("-- not handled"); } // Nothing to handle control then return false; } global func InitializePlayerControl(int plr, string controlset_name, bool keyboard, bool mouse, bool gamepad) { // VC = VirtualCursor if(!CON_VC_Players) CON_VC_Players = CreateArray(); CON_VC_Players[plr] = !mouse; // for all clonks... for (var clonk in FindObjects(Find_OCF(OCF_CrewMember))) { clonk->~ReinitializeControls(); } } global func PlayerHasVirtualCursor(int plr) { if (!CON_VC_Players) return false; return CON_VC_Players[plr]; } // Control2Player // Player-wide controls global func Control2Player(int plr, int ctrl, int x, int y, int strength, bool repeat, int status) { // select previous or next if (ctrl == CON_PreviousCrew) return ShiftCursor(plr, true); if (ctrl == CON_NextCrew) return ShiftCursor(plr, false); // all those hotkeys... var hotkey = 0; if (ctrl == CON_PlayerHotkey0) hotkey = 10; if (ctrl == CON_PlayerHotkey1) hotkey = 1; if (ctrl == CON_PlayerHotkey2) hotkey = 2; if (ctrl == CON_PlayerHotkey3) hotkey = 3; if (ctrl == CON_PlayerHotkey4) hotkey = 4; if (ctrl == CON_PlayerHotkey5) hotkey = 5; if (ctrl == CON_PlayerHotkey6) hotkey = 6; if (ctrl == CON_PlayerHotkey7) hotkey = 7; if (ctrl == CON_PlayerHotkey8) hotkey = 8; if (ctrl == CON_PlayerHotkey9) hotkey = 9; if (hotkey > 0) { // valid crew number? var crew = GetCrew(plr,hotkey-1); if (!crew) return false; // stop previously selected crew StopSelected(); // set cursor if not disabled etc. return SetCursor(plr, crew); } // Modifier keys - do not handle the key. The GetPlayerControlState will still return the correct value when the key is held down. if (ctrl == CON_ModifierMenu1) return false; // cursor pos info - store in player values if (ctrl == CON_CursorPos) { if (!g_player_cursor_pos) g_player_cursor_pos = CreateArray(plr+1); g_player_cursor_pos[plr] = [x, y]; return true; } /* if (ctrl == CON_Test) { Message(Format("%d %d (%d %d) %d [%d %d]", plr, ctrl, x, y, strength, repeat, release)); return true; } */ return false; } /* return info of last sent CON_CursorPos packet for that player as [x, y] */ global func GetPlayerCursorPos(int plr) { if (!g_player_cursor_pos) return 0; return g_player_cursor_pos[plr]; } global func StopSelected(int plr) { var cursor = GetCursor(plr); if (cursor) { cursor->SetCommand("None"); cursor->SetComDir(COMD_Stop); } } /* Object functions */ // To be called in an object context only! // Control2Effect // Call control function in all effects that have "Control" in their name global func Control2Effect(int plr, int ctrl, int x, int y, int strength, bool repeat, int status) { // x and y are local coordinates if (!this) return false; // Count down from EffectCount, in case effects get deleted var i = GetEffectCount("*Control*", this), fx; while (i--) { fx = GetEffect("*Control*", this, i); if (fx) if (EffectCall(this, fx, "Control", ctrl, x,y,strength, repeat, status)) return true; } // No effect handled the control return false; } // ObjectControl // Called from PlayerControl when a control is issued to the cursor // Return whether handled // To be overloaded by specific objects to enable additional controls global func ObjectControl(int plr, int ctrl, int x, int y, int strength, bool repeat, int status) { if (!this) return false; // Any control resets a previously given command SetCommand("None"); // Movement controls if (ctrl == CON_Left || ctrl == CON_Right || ctrl == CON_Up || ctrl == CON_Down || ctrl == CON_Jump) return ObjectControlMovement(plr, ctrl, strength, status, repeat); // Unhandled control return false; } // Find an object with an entrance in front of this object whose entrance is at // the right position global func GetEntranceObject() { if (!this) return nil; // object with an entrance on target position var obj = FindObject(Find_OCF(OCF_Entrance), Find_Layer(GetObjectLayer()), Find_AtPoint(0,0), Find_Exclude(this)); if (!obj) return nil; var x = obj->GetDefCoreVal("Entrance","DefCore",0) + obj->GetX(); var y = obj->GetDefCoreVal("Entrance","DefCore",1) + obj->GetY(); var wdt = obj->GetDefCoreVal("Entrance","DefCore",2); var hgt = obj->GetDefCoreVal("Entrance","DefCore",3); // entrance is on the vehicle? if (!Inside(GetX(), x, x+wdt)) return nil; if (!Inside(GetY(), y, y+hgt)) return nil; return obj; } global func NameComDir(comdir) { if(comdir == COMD_Stop) return "COMD_Stop"; if(comdir == COMD_Up) return "COMD_Up"; if(comdir == COMD_UpRight) return "COMD_UpRight"; if(comdir == COMD_UpLeft) return "COMD_UpLeft"; if(comdir == COMD_Right) return "COMD_Right"; if(comdir == COMD_Left) return "COMD_Left"; if(comdir == COMD_Down) return "COMD_Down"; if(comdir == COMD_DownRight) return "COMD_DownRight"; if(comdir == COMD_DownLeft) return "COMD_DownLeft"; if(comdir == COMD_None) return "COMD_None"; } // Called when CON_Left/Right/Up/Down controls are issued/released // Return whether handled global func ObjectControlMovement(int plr, int ctrl, int strength, int status, bool repeat) { if (!this) return false; // movement is only possible when not contained if (Contained()) return false; // this is for controlling movement with Analogpad if (status == CONS_Down) if (strength != nil && strength < CON_Gamepad_Deadzone) return true; var proc = GetProcedure(); // Some specific movement controls if (status == CONS_Down) { // Jump control if (ctrl == CON_Jump) { if (proc == "WALK" && GetComDir() == COMD_Up) SetComDir(COMD_Stop); if (proc == "WALK") { this->ObjectCommand("Jump"); return true; } } if (proc == "SWIM" && !GBackSemiSolid(0,-5)) //Water jump { if (ctrl == CON_Up) return false; else if(ctrl == CON_Jump) this->ObjectCommand("Jump"); } if (proc == "SCALE") // Let go from scaling a wall { //Wall kick if (ctrl == CON_Left && GetDir() == DIR_Right && GetComDir() == COMD_Up) return this->ObjectCommand("Jump"); if (ctrl == CON_Right && GetDir() == DIR_Left && GetComDir() == COMD_Up) return this->ObjectCommand("Jump"); //Let go of wall if (ctrl == CON_Left && GetDir() == DIR_Right) return this->ObjectComLetGo(-10); if (ctrl == CON_Right && GetDir() == DIR_Left) return this->ObjectComLetGo(+10); } else if (proc == "HANGLE") // Let go from hangling the ceiling { if (ctrl == CON_Down) return this->ObjectComLetGo(0,0); } // Direct turnaround if object is standing still. Valid for any procedure in OC if (!GetXDir()) { if (ctrl == CON_Left) SetDir(DIR_Left); else if (ctrl == CON_Right) SetDir(DIR_Right); } } else // release { // If rolling, allow to instantly switch to walking again. if (GetAction() == "Roll") { if (ctrl == CON_Left && GetDir() == DIR_Left || ctrl == CON_Right && GetDir() == DIR_Right) SetAction("Walk"); } } return ObjectControlUpdateComdir(plr); } // Updates ComDir of object based on current Con_*-directional controls // Return whether actual, effective direction of movement changed global func ObjectControlUpdateComdir(int plr) { if (!this) return false; // Generic movement: Update ComDir based on current control state var new_comdir = GetPlayerConDir(plr, CON_Left, CON_Up, CON_Right, CON_Down); var old_comdir = GetComDir(); if (new_comdir != old_comdir) { // ComDir changed. Update. SetComDir(new_comdir); //var s = ""; //if (GetPlayerControlState(plr, CON_Left)) s = Format("%sL", s); //if (GetPlayerControlState(plr, CON_Up)) s = Format("%sU", s); //if (GetPlayerControlState(plr, CON_Right)) s = Format("%sR", s); //if (GetPlayerControlState(plr, CON_Down)) s = Format("%sD", s); //s = Format("%s %s", s, ["Stop", "Up", "UpRight", "Right", "DownRight", "Down", "DownLeft", "Left", "UpLeft"][new_comdir]); //Message("@%s", this, s); // The control is only handled if it had an actual effect on the current movement direction of the Clonk var old = ComDir2XY(old_comdir); var new = ComDir2XY(new_comdir); var old_cx = old[0], old_cy = old[1], new_cx = new[0], new_cy = new[1]; var is_handled; var proc = GetProcedure(); if (proc == "WALK" || proc == "HANGLE" || proc == "PUSH" || proc == "PULL" || proc == "FLIGHT") // Only horizontal movement changed actual direction // Also, enforce clear Left/Right commands without leftover Up/Down // CON_Down is never handled this way, thus forcing a CON_Stop is_handled = (old_cx != new_cx) && !new_cy; else if (proc == "SCALE") // Only vertical movement changed actual direction // Also, enfore clear Up/Down to prevent "Zuppel" in corner is_handled = (old_cy != new_cy) && !new_cx; else if (proc == "SWIM" || proc == "FLOAT" || proc == "DIG") is_handled = (old_cx != new_cx || old_cy != new_cy); // Free 360 degree movement else is_handled = false; return is_handled; } else { // ComDir did not change. -> The control was not handled //Log("NoChange"); return false; } } // selects the next/previous crew member (that is not disabled) global func ShiftCursor(int plr, bool back, bool force) { // Is the selected Clonk busy at the moment? E.g. uncloseable menu open.. if (!force) { var cursor = GetCursor(plr); if (cursor && cursor->~RejectShiftCursor()) return false; } // get index of currently selected crew var index = 0; while (index < GetCrewCount(plr)) { if (GetCursor(plr) == GetCrew(plr,index)) break; index++; } // a short explanation here: // when shifting the cursor to the next crew member, this crew member // might be disabled via SetCrewEnabled. That is why we have to go to // the crew after the next and perhaps even the crew after the next // after the next. Also, we need to stop this skipping after all crew // members have been checked in the special case that all crew members // are disabled var maxcycle = GetCrewCount(plr); var cycle = 0; do { if (back) { --index; if (index < 0) index = GetCrewCount(plr)-1; } else { ++index; if (index >= GetCrewCount(plr)) index = 0; } ++cycle; } while (cycle < maxcycle && !(GetCrew(plr,index)->GetCrewEnabled())); // Changing the cursor closes all menus that are associated with the old cursor. // However, if a menu is not closable, then it requires the attention of the player and switching the cursor is disabled.. var current_cursor = GetCursor(plr); var new_cursor = GetCrew(plr, index); if (current_cursor == new_cursor) return false; StopSelected(plr); if (current_cursor) current_cursor->~OnShiftCursor(new_cursor); return SetCursor(plr, new_cursor); } // Temporarily used for Debugging! // Helper function to turn CON_*-constants into strings global func GetPlayerControlName(int ctrl) { var con_name = GetConstantNameByValue(ctrl, "CON_"); if (!con_name) con_name = Format("Unknown(%d)", ctrl); return con_name; } // Return COMD_*-constant corresponding to current state of passed directional controls global func GetPlayerConDir(int plr, int con_left, int con_up, int con_right, int con_down) { var x,y; if (GetPlayerControlState(plr, con_left)) --x; if (GetPlayerControlState(plr, con_up)) --y; if (GetPlayerControlState(plr, con_right)) ++x; if (GetPlayerControlState(plr, con_down)) ++y; // Creating an array here for every keypress/release // Would be so cool to have this static const. Guenther? var dir_coms = [COMD_UpLeft, COMD_Up, COMD_UpRight, COMD_Left, COMD_Stop, COMD_Right, COMD_DownLeft, COMD_Down, COMD_DownRight]; return dir_coms[y*3+x+4]; } // Returns coordinate directions associated with a COMD_Constant global func ComDir2XY(int comd) { // Creating an array here for every keypress/release // Would be so cool to have this static const. Guenther? return [[0,0,1,1,1,0,-1,-1,-1][comd], [0,-1,-1,0,1,1,1,0,-1][comd]]; } global func ObjectCommand(string command, object target, int tx, int ty, object target2, /*any*/ data) { // this function exists to be overloadable by ClonkControl.c4d if (!this) return; this->SetCommand(command, target, tx, ty, target2, data); } // Let go from scaling or hangling global func ObjectComLetGo(int vx, int vy) { if (!SetAction("Jump")) return false; SetXDir(vx); SetYDir(vy); return true; } /* Mouse Hovering */ // Engine callback when the mouse hovers over an object (entering) or stops hovering over an object (leaving). // Either leaving, entering or both are set. They can be nil, but never both at the same time. // dragged is an object being drag(¬ dropped yet) global func MouseHover(int player, object leaving, object entering, object dragged) { // Leaving the hovering zone should be processed first. if (leaving) leaving->~OnMouseOut(player, dragged); // Then process entering a new hovering zone. if (entering) entering->~OnMouseOver(player, dragged); return true; } /* Drag & Drop */ // Engine callback on drag&drop: gives the player, the dragged obect and the object which is being dropped(can be nil). global func MouseDragDrop(int plr, object source, object target) { //Log("MouseDragDrop(%d, %s, %s)", plr, source->GetName(), target->GetName()); if (!source) return false; // can happen if source got deleted after control was queued var src_drag = source->~OnMouseDrag(plr); if (!src_drag) return false; // If there is drop target, check whether it accepts the drop. if (target) if (!target->~OnMouseDrop(plr, src_drag)) return false; // Notify the drop object it succeeded. if (source) source->~OnMouseDragDone(src_drag, target); return true; } /* Debug */ // uncomment this to get log messages for any player control issued /*global func PlayerControl(int plr, int ctrl, id spec_id, int x, int y, int strength, bool repeat, bool release) { var r = inherited(plr, ctrl, spec_id, x, y, strength, repeat, release, ...), rs; if (r) rs = ""; else rs = "!"; Log("%s%d, %s, %i, %d, %d, %d, %v, %v", rs, plr, GetPlayerControlName(ctrl), spec_id, x,y,strength, repeat, release); return r; }*/ /* This is used by Library_ClonkInventoryControl and needs to be a global function (in a non-appendto). This function returns the priority of an object when selecting an object to pick up. */ global func Library_ClonkInventoryControl_Sort_Priority(int x_position) { // Objects are sorted by position, preferring the direction of the key press. var priority_x = GetX() - x_position; if (priority_x < 0) priority_x += 1000; return priority_x; }