diff --git a/README.md b/README.md index ec18a13..f0de291 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ -# Client_RPG - -Top-down client for GameMode_RPG \ No newline at end of file +# Client_RPG + +Required client add-on for **GameMode_RPG**: top-down orbit camera, click-to-move/combat, HUD and `/rpg` window hotkeys. + +Install into `Add-Ons/Client_RPG` (same folder name as on the server). + +## Controls + +| Input | Action | +|---|---| +| Left click | Walk / attack / interact (server resolves the ray) | +| Right click | Talk / context | +| A/D, arrows | Rotate camera | +| PgUp/PgDn, +/- | Zoom | +| B / K / E | Bag / skills / equip lists | +| R | Toggle auto-retaliate | + +Protocol version **2** — must match `GameMode_RPG` ui/protocol.cs. diff --git a/client.cs b/client.cs new file mode 100644 index 0000000..10ad315 --- /dev/null +++ b/client.cs @@ -0,0 +1,367 @@ +// Client_RPG — top-down camera, click-to-move, and RPG windows for GameMode_RPG. +exec("./ui.cs"); +// The server probes with RPG_Probe; reply with serverCmdRPG_Hello(2) to play. + +$RPGClient::Version = 2; +$RPGClient::Active = 0; +$RPGClient::Yaw = 0; +$RPGClient::Dist = 14; + +function RPGClient_NormPos(%ctrl, %pos) +{ + if (!isObject(%ctrl)) + { + return "0.5 0.5"; + } + %ext = %ctrl.getExtent(); + %w = getWord(%ext, 0); + %h = getWord(%ext, 1); + if (%w < 1) + { + %w = 1; + } + if (%h < 1) + { + %h = 1; + } + %x = getWord(%pos, 0) / %w; + %y = 1 - getWord(%pos, 1) / %h; + if (%x < 0) + { + %x = 0; + } + if (%x > 1) + { + %x = 1; + } + if (%y < 0) + { + %y = 0; + } + if (%y > 1) + { + %y = 1; + } + return %x SPC %y; +} + +function RPGClient_Aspect() +{ + if (!isObject(Canvas)) + { + return 1.333; + } + %ext = Canvas.getExtent(); + %w = getWord(%ext, 0); + %h = getWord(%ext, 1); + return %h > 0 ? %w / %h : 1.333; +} + +function RPGClient_SendClick(%button, %pos) +{ + if (!isObject(serverConnection)) + { + return; + } + %norm = RPGClient_NormPos(RPGClient_Mouse, %pos); + commandToServer('RPG_Click', %button, getWord(%norm, 0), getWord(%norm, 1), RPGClient_Aspect()); +} + +function RPGClient_Mouse::onMouseDown(%this, %modifier, %mouseClick, %mousePos, %button) +{ + if (!$RPGClient::Active) + { + return; + } + RPGClient_SendClick(%button, %mousePos); +} + +function RPGClient_Mouse::onRightMouseDown(%this, %modifier, %mouseClick, %mousePos, %button) +{ + if (!$RPGClient::Active) + { + return; + } + RPGClient_SendClick(1, %mousePos); +} + +function RPGClient_EnableOverlay() +{ + if (!isObject(PlayGui) || isObject(RPGClient_Mouse)) + { + return; + } + RPGClient_Mouse = new GuiMouseEventCtrl(RPGClient_Mouse) + { + horizSizing = "width"; + vertSizing = "height"; + position = "0 0"; + extent = "640 480"; + profile = "GuiDefaultProfile"; + visible = "1"; + enabled = "1"; + }; + PlayGui.add(RPGClient_Mouse); + RPGClient_Mouse.pushToBack(); + if (isObject(RPGClient_KeyMap)) + { + RPGClient_KeyMap.delete(); + } + RPGClient_KeyMap = new ActionMap("RPGClient_KeyMap"); + RPGClient_KeyMap.bind(keyboard0, a, RPGClient_YawLeft); + RPGClient_KeyMap.bind(keyboard0, d, RPGClient_YawRight); + RPGClient_KeyMap.bind(keyboard0, left, RPGClient_YawLeft); + RPGClient_KeyMap.bind(keyboard0, right, RPGClient_YawRight); + RPGClient_KeyMap.bind(keyboard0, pgup, RPGClient_ZoomIn); + RPGClient_KeyMap.bind(keyboard0, pgdn, RPGClient_ZoomOut); + RPGClient_KeyMap.bind(keyboard0, equals, RPGClient_ZoomIn); + RPGClient_KeyMap.bind(keyboard0, minus, RPGClient_ZoomOut); + Canvas.pushDialog(RPGClient_Mouse, 0); + RPGClient_KeyMap.push(); + if (isObject(PlayGui)) + { + PlayGui.killToolHud(); + } +} + +function RPGClient_DisableOverlay() +{ + if (isObject(RPGClient_KeyMap)) + { + RPGClient_KeyMap.pop(); + } + if (isObject(RPGClient_Mouse)) + { + Canvas.popDialog(RPGClient_Mouse); + RPGClient_Mouse.delete(); + } + $RPGClient::Active = 0; +} + +function RPGClient_ApplyCamera() +{ + if (!isObject(localClientConnection) || !isObject(localClientConnection.player)) + { + return; + } + %body = localClientConnection.player; + %cam = localClientConnection.getControlObject(); + if (!isObject(%cam) || %cam.getClassName() !$= "Camera") + { + %cam = localClientConnection.camera; + } + if (!isObject(%cam)) + { + return; + } + localClientConnection.setControlObject(%cam); + %rot = "0 0" SPC mSin($RPGClient::Yaw / 2) SPC mCos($RPGClient::Yaw / 2); + %cam.setOrbitMode(%body, %body.getTransform(), 4, 32, $RPGClient::Dist); + %cam.thirdPersonOnly = 1; + commandToServer('RPG_CamSync', $RPGClient::Yaw, $RPGClient::Dist); +} + +function RPGClient_YawLeft(%val) +{ + if (!$RPGClient::Active || %val == 0) + { + return; + } + $RPGClient::Yaw = $RPGClient::Yaw - 0.06; + RPGClient_ApplyCamera(); +} + +function RPGClient_YawRight(%val) +{ + if (!$RPGClient::Active || %val == 0) + { + return; + } + $RPGClient::Yaw = $RPGClient::Yaw + 0.06; + RPGClient_ApplyCamera(); +} + +function RPGClient_ZoomIn(%val) +{ + if (!$RPGClient::Active || %val == 0) + { + return; + } + $RPGClient::Dist = getMax(6, $RPGClient::Dist - 0.6); + RPGClient_ApplyCamera(); +} + +function RPGClient_ZoomOut(%val) +{ + if (!$RPGClient::Active || %val == 0) + { + return; + } + $RPGClient::Dist = getMin(32, $RPGClient::Dist + 0.6); + RPGClient_ApplyCamera(); +} + +function clientCmdRPG_Probe() +{ + if (isObject(serverConnection)) + { + commandToServer('RPG_Hello', $RPGClient::Version); + } +} + +function clientCmdRPG_Welcome(%serverProto, %accepted) +{ + if (%accepted && %serverProto == $RPGClient::Version) + { + $RPGClient::Server = 1; + } +} + +function clientCmdRPG_TopDown(%on, %yaw, %dist) +{ + if (%on) + { + $RPGClient::Active = 1; + $RPGClient::Yaw = %yaw; + $RPGClient::Dist = %dist > 0 ? %dist : 14; + RPGClient_EnableOverlay(); + RPGClient_BindKeys(); + schedule(50, 0, RPGClient_ApplyCamera); + schedule(200, 0, RPGClient_ApplyCamera); + } + else + { + RPGClient_DisableOverlay(); + RPGClient_UnbindKeys(); + } +} + +function clientCmdRPG_Hud(%hp, %maxHp, %silver, %load, %cap, %skillId, %skillValue, %blocking, %tHp, %tMax) +{ + RPGClient_Hud_Update(%hp, %maxHp, %silver, %load, %cap, %skillId, %skillValue, %tHp, %tMax); +} + +function clientCmdRPG_HudOff() +{ + RPGClient_Hud_Hide(); +} + +function clientCmdRPG_Notify(%text, %sec) +{ + if (%text !$= "") + { + RPGClient_Notify_Show(%text, %sec); + } +} + +function clientCmdRPG_Dialog(%title, %body, %sec) +{ + RPGClient_Dialog_Show(%title, %body, %sec); +} + +function clientCmdRPG_ListBegin(%title, %page, %pages) +{ + RPGClient_List_Begin(%title, %page, %pages); +} + +function clientCmdRPG_ListRow(%row) +{ + RPGClient_List_Row(%row); +} + +function clientCmdRPG_ListEnd() +{ + RPGClient_List_End(); +} + +// --- HUD / list / dialog (Phase 4): thin windows on PlayGui --- +function RPGClient_Hud_Ensure() +{ + if (isObject(RPGClient_Hud)) + { + return; + } + RPGClient_Hud = new GuiTextCtrl() + { + horizSizing = "width"; + vertSizing = "top"; + position = "8 8"; + extent = "600 24"; + profile = "ChatHudMessageProfile"; + text = ""; + }; + PlayGui.add(RPGClient_Hud); +} + +function RPGClient_Hud_Update(%hp, %maxHp, %silver, %load, %cap, %skillId, %skillValue, %tHp, %tMax) +{ + if (!$RPGClient::Active) + { + return; + } + RPGClient_Hud_Ensure(); + %t = ""; + if (%tMax > 0) + { + %t = " | Target " @ %tHp @ "/" @ %tMax; + } + RPGClient_Hud.setText("HP " @ %hp @ "/" @ %maxHp @ %t @ " | " @ %silver @ " silver | load " @ %load @ "/" @ %cap @ " | " @ %skillId @ " " @ %skillValue); +} + +function RPGClient_Hud_Hide() +{ + if (isObject(RPGClient_Hud)) + { + RPGClient_Hud.setText(""); + } +} + +function RPGClient_Notify_Show(%text, %sec) +{ + RPGClient_Hud_Ensure(); + RPGClient_Hud.setText(%text); + if (%sec > 0) + { + schedule(%sec * 1000, RPGClient_Hud, setText, ""); + } +} + +function RPGClient_Dialog_Show(%title, %body, %sec) +{ + messageBoxOK(%title, %body, ""); +} + +function RPGClient_List_Begin(%title, %page, %pages) +{ + if (isObject(RPGClient_List)) + { + RPGClient_List.delete(); + } + RPGClient_List = ""; + RPGClient_ListTitle = %title @ " (" @ (%page + 1) @ "/" @ %pages @ ")"; +} + +function RPGClient_List_Row(%row) +{ + RPGClient_List = RPGClient_List @ %row @ "\n"; +} + +function RPGClient_List_End() +{ + if (RPGClient_List !$= "") + { + messageBoxOK(RPGClient_ListTitle, RPGClient_List, ""); + } +} + +package RPGClient_Disconnect +{ + function disconnect() + { + RPGClient_DisableOverlay(); + Parent::disconnect(); + } +}; +activatePackage(RPGClient_Disconnect); + +echo("Client_RPG loaded (protocol " @ $RPGClient::Version @ ")"); diff --git a/description.txt b/description.txt new file mode 100644 index 0000000..295e9a2 --- /dev/null +++ b/description.txt @@ -0,0 +1,2 @@ +Client_RPG +Required client add-on for the GameMode_RPG realm: top-down camera, click-to-move and combat, and the RPG windows. Without it you can only spectate on an RPG server. diff --git a/ui.cs b/ui.cs new file mode 100644 index 0000000..574da5a --- /dev/null +++ b/ui.cs @@ -0,0 +1,72 @@ +// Client windows: hotkeys open /rpg lists; the server still validates every command. + +$RPGClient::MenuOpen = 0; + +function RPGClient_RunCmd(%cmd) +{ + if (!isObject(serverConnection)) + { + return; + } + commandToServer('message', "/rpg " @ %cmd); +} + +function RPGClient_ToggleMenu(%name, %cmd) +{ + if ($RPGClient::MenuOpen $= %name) + { + $RPGClient::MenuOpen = ""; + return; + } + $RPGClient::MenuOpen = %name; + RPGClient_RunCmd(%cmd); +} + +function RPGClient_Menu_Bag() +{ + RPGClient_ToggleMenu("bag", "bag"); +} + +function RPGClient_Menu_Skills() +{ + RPGClient_ToggleMenu("skills", "skills"); +} + +function RPGClient_Menu_Equip() +{ + RPGClient_ToggleMenu("equip", "equip"); +} + +function RPGClient_Menu_Help() +{ + RPGClient_RunCmd("help"); +} + +function RPGClient_Menu_Retaliate() +{ + RPGClient_RunCmd("retaliate"); +} + +function RPGClient_BindKeys() +{ + if (isObject(RPGClient_MenuMap)) + { + RPGClient_MenuMap.delete(); + } + RPGClient_MenuMap = new ActionMap("RPGClient_MenuMap"); + RPGClient_MenuMap.bind(keyboard0, b, RPGClient_Menu_Bag); + RPGClient_MenuMap.bind(keyboard0, k, RPGClient_Menu_Skills); + RPGClient_MenuMap.bind(keyboard0, e, RPGClient_Menu_Equip); + RPGClient_MenuMap.bind(keyboard0, f1, RPGClient_Menu_Help); + RPGClient_MenuMap.bind(keyboard0, r, RPGClient_Menu_Retaliate); + RPGClient_MenuMap.push(); +} + +function RPGClient_UnbindKeys() +{ + if (isObject(RPGClient_MenuMap)) + { + RPGClient_MenuMap.pop(); + RPGClient_MenuMap.delete(); + } +}