* Skullball (UOX3 JavaScript)
* SpiderMonkey 1.8.5 (ES5-ish)
*
* Lightweight port of the classic RunUO "Skullball" minigame to UOX3.
* Focus: a pushable ball, two goal mouths (Silver vs Gold), score tracking, optional
* player freeze on kickoff, and simple rewards.
*
* Install
* - Add a DFN item that uses sectionID [skullball_ball], [skullball_goal_silver], [skullball_goal_gold],
* [skullball_controller], [skullball_gate_enter], [skullball_gate_exit] and attach this script via EVENT=.
* You can also spawn via the GM command this script registers: 'skullball setup
* - Add to jse_fileassociations.scp under [SCRIPT_LIST] with a script ID (e.g. 7555)
* - Optionally tweak constants below.
*
* Notes
* - Ball is moved by player collision (onCollide) – a simple "kick" one tile or short slide.
* - Goals are detected by checking the ball's tile for goal items after each move.
* - A single Controller item persists arena state via tags (scores, spawn points, options).
* - Rewards are simple trophy items created in winners' backpacks (configurable).
*
* Script IDs used here: 7555 (adjust if needed)
*/
[skullball_ball]
{
get=base_item
name=a skullball
id=6880
DECAY=0
COLOR=1152
SCRIPT=7555
}
[skullball_gate_enter]
{
get=base_item
name=Enter Skullball
id=0xF6C
DECAY=0
DIRECTION=1
SCRIPT=7555
}
[skullball_gate_exit]
{
get=base_item
name=Exit Skullball
id=0xF6C
DECAY=0
DIRECTION=1
SCRIPT=7555
}
[skullball_controller]
{
get=base_item
name=Skullball Setup Stone
id=4483
DECAY=0
COLOR=33
SCRIPT=7555
}
{
get=base_item
name=a skullball
id=6880
DECAY=0
COLOR=1152
SCRIPT=7555
}
[skullball_gate_enter]
{
get=base_item
name=Enter Skullball
id=0xF6C
DECAY=0
DIRECTION=1
SCRIPT=7555
}
[skullball_gate_exit]
{
get=base_item
name=Exit Skullball
id=0xF6C
DECAY=0
DIRECTION=1
SCRIPT=7555
}
[skullball_controller]
{
get=base_item
name=Skullball Setup Stone
id=4483
DECAY=0
COLOR=33
SCRIPT=7555
}
// ---------------------
// Constants / defaults
// ---------------------
var SCRIPT_ID = 7555;
var TAG_CTRL = "skullball_ctrl"; // tag placed on controller item (value "1")
var TAG_IS_GOAL = "skullball_goal"; // tag placed on goal mouth items: value "silver" or "gold"
var TAG_IS_BALL = "skullball_ball"; // tag placed on ball item: value "1"
var CTRL_DEFAULTS = {
giveReward: 1, // 1/0
freezePlayers: 1, // 1/0
freezeTimeMs: 3000, // milliseconds
rewardMinTeam: 1, // minimum members per team to award
showDateOnReward: 1, // 1/0
showNameOnReward: 1, // 1/0
showTeamSizeOnReward: 1 // 1/0
};
var HUE_SILVER = 1150;
var HUE_GOLD = 1161;
var HUE_REF = 1153;
var KICK_MAX_STEPS = 2; // how many tiles a kick can push the ball
var KICK_SLIDE_MS = 75; // delay between slide steps (visual nicety)
// ---------------------
// Helpers
// ---------------------
function _toInt(v) { var n = parseInt(v, 10); return isNaN(n) ? 0 : n; }
function _toBool01(v) { return (v|0) ? 1 : 0; }
function _getTagInt(o, k, d) {
var raw = o.GetTag(k);
if (raw === null || raw === undefined || raw === "") return (d|0);
var n = parseInt(raw, 10);
return isNaN(n) ? (d|0) : n;
}
function _getTagStr(o, k, d) { var v = o.GetTag(k); return (v===null || v===undefined || v === "") ? (d||"") : (""+v); }
function _setTag(o, k, v) { o.SetTag(k, v); }
function _setTempTag(o, k, v) { o.SetTempTag(k, v); }
function _clrTag(o, k) { o.DelTag(k); }
// Safe serial -> item resolver (handles shard differences)
function _itemFromSer(ser)
{
ser |= 0; if (ser <= 0) return null;
if (typeof CalcItemFromSer === "function") return CalcItemFromSer(ser);
if (typeof CalcItemBySerial === "function") return CalcItemBySerial(ser);
return null;
}
function AFN_FindCtrl(srcChar, trgItem, pSock)
{
if (trgItem.GetTag(TAG_CTRL) == "1") { pSock.tempInt1 = trgItem.serial; return true; }
return false;
}
function _ctrlFindNear(pivot, sock)
{
var useSock = sock || (pivot && pivot.socket) || null;
if (!useSock) return null;
useSock.tempInt1 = 0;
AreaItemFunction("AFN_FindCtrl", pivot, 20, useSock);
var ser = useSock.tempInt1|0; if (ser <= 0) return null;
var found = CalcItemFromSer ? CalcItemFromSer(ser) : null;
return found;
}
function _ctrlEnsure(pivot) {
var c = _ctrlFindNear(pivot, pivot.socket);
if (!ValidateObject(c)) {
c = CreateBlankItem(pivot.socket, pivot, 1, "Skullball Controller", 0x1F14, 0x0000, "ITEM", false); // invisible controller
c.visible = 0; c.movable = 0; c.name = "Skullball Controller"; c.hue = 0x0000;
_setTag(c, TAG_CTRL, "1");
// defaults
_setTag(c, "silverScore", 0);
_setTag(c, "goldScore", 0);
_setTag(c, "freezePlayers", CTRL_DEFAULTS.freezePlayers);
_setTag(c, "freezeTimeMs", CTRL_DEFAULTS.freezeTimeMs);
_setTag(c, "giveReward", CTRL_DEFAULTS.giveReward);
_setTag(c, "rewardMinTeam", CTRL_DEFAULTS.rewardMinTeam);
_setTag(c, "showDateOnReward", CTRL_DEFAULTS.showDateOnReward);
_setTag(c, "showNameOnReward", CTRL_DEFAULTS.showNameOnReward);
_setTag(c, "showTeamSizeOnReward", CTRL_DEFAULTS.showTeamSizeOnReward);
_setTag(c, "maxPoints", CTRL_DEFAULTS.maxPoints||5);
_setTag(c, "matchOver", 0);
}
// ensure defaults exist on old controllers
if (c.GetTag("maxPoints") === null || c.GetTag("maxPoints") === undefined || c.GetTag("maxPoints") === "")
_setTag(c, "maxPoints", CTRL_DEFAULTS.maxPoints||5);
if (c.GetTag("matchOver") === null || c.GetTag("matchOver") === undefined || c.GetTag("matchOver") === "")
_setTag(c, "matchOver", 0);
return c;
}
function _ctrlFromBall(ball)
{
if (!ValidateObject(ball)) return null;
var s = _toInt(ball.GetTag("ctrlSer"));
if (s > 0) { var c = _itemFromSer(s); if (ValidateObject(c)) return c; }
var sock = (ball.container && ball.container.socket) || null;
if (sock) {
sock.tempInt1 = 0; AreaItemFunction("AFN_FindCtrl", ball, 40, sock);
var ser = sock.tempInt1|0; if (ser > 0) { ball.SetTag("ctrlSer", ser); return _itemFromSer(ser); }
}
return null;
}
function _createBall(pUser) {
var c = _ctrlEnsure(pUser);
var ball = CreateBlankItem(pUser.socket, pUser, 1, "Skullball", 6880, 1152, "ITEM", false); // skull
ball.name = "Skullball";
ball.movable = 0;
_setTag(ball, TAG_IS_BALL, "1");
ball.AddScriptTrigger(SCRIPT_ID);
ball.Refresh();
_setTag(c, "ballSerial", ball.serial);
ball.SetTag("ctrlSer", c.serial|0);
_setTag(c, "centerX", pUser.x);
_setTag(c, "centerY", pUser.y);
_setTag(c, "centerZ", pUser.z);
_setTag(c, "world", pUser.worldNumber);
_setTag(c, "instance", pUser.instanceID);
pUser.SysMessage("Skullball placed. This tile set as center.");
return ball;
}
function _createGoal(pUser, team) {
var art = 0x1B73; // skull pike as a placeholder goal marker
var i = CreateBlankItem(pUser.socket, pUser, 1, (team === "silver" ? "Silver" : "Gold") + " Goal", art, (team === "silver" ? HUE_SILVER : HUE_GOLD), "ITEM", false);
i.name = (team === "silver" ? "Silver" : "Gold") + " Goal";
i.hue = (team === "silver" ? HUE_SILVER : HUE_GOLD);
i.movable = 0;
_setTag(i, TAG_IS_GOAL, team);
i.AddScriptTrigger(SCRIPT_ID);
i.Refresh();
var c = _ctrlEnsure(pUser);
_setTag(c, team+"GoalX", pUser.x);
_setTag(c, team+"GoalY", pUser.y);
_setTag(c, team+"GoalZ", pUser.z);
_setTag(c, team+"GoalSerial", i.serial);
pUser.SysMessage((team === "silver"?"Silver":"Gold") + " goal placed.");
return i;
}
function AFN_FindGoalAt(srcObj, trgItem, pSock)
{
var tx = pSock.tempInt2|0, ty = pSock.tempInt3|0;
if (trgItem.x === tx && trgItem.y === ty && trgItem.GetTag(TAG_IS_GOAL)) { pSock.tempInt1 = trgItem.serial; return true; }
return false;
}
function _findGoalAt(pivotItem, x, y, sock)
{
var useSock = sock || (pivotItem && pivotItem.container && pivotItem.container.socket) || null;
if (!useSock || !ValidateObject(pivotItem)) return null;
useSock.tempInt1 = 0; useSock.tempInt2 = x|0; useSock.tempInt3 = y|0;
AreaItemFunction("AFN_FindGoalAt", pivotItem, 0, useSock);
var ser = useSock.tempInt1|0; if (ser <= 0) return null;
return _itemFromSer(ser);
}
function _awardTeam(ctrl, team, players) {
if (!_toInt(ctrl.GetTag("giveReward"))) return;
var minTeam = _toInt(ctrl.GetTag("rewardMinTeam"));
if (!players || players.length < minTeam) return;
for (var i = 0; i < players.length; i++) {
var ch = players[i];
if (!ValidateObject(ch) || !ch.isChar || ch.npc) continue;
var pack = ch.pack;
if (!ValidateObject(pack)) continue;
var trophy = CreateBlankItem(ch.socket, ch, 1, "Skullball Trophy", 0x0E2D, (team==="silver"?HUE_SILVER:HUE_GOLD), "ITEM", true); // trophy to backpack
trophy.hue = (team=="silver"?HUE_SILVER:HUE_GOLD);
var parts = [ team.toUpperCase()+" SKULLBALL TROPHY" ];
if (_toInt(ctrl.GetTag("showNameOnReward"))) parts.push("- "+ch.name);
if (_toInt(ctrl.GetTag("showTeamSizeOnReward"))) parts.push("(Team "+players.length+")");
if (_toInt(ctrl.GetTag("showDateOnReward"))) parts.push("- "+new Date().toDateString());
trophy.name = parts.join(" ");
}
}
function _freeze(ch, ms) {
if (!ValidateObject(ch) || !ch.isChar) return;
ch.Freeze(_toInt(ms));
}
function _kickBall(pUser, ball, ctrl, dx, dy) {
if (!ValidateObject(ball)) return;
// Store slide state on the ball and start a timer that calls back into THIS script
ball.SetTag("sb_dx", (dx|0));
ball.SetTag("sb_dy", (dy|0));
ball.SetTag("sb_steps", (KICK_MAX_STEPS|0));
var cser = _toInt(ball.GetTag("ctrlSer"));
if (!cser && ValidateObject(ctrl)) cser = (ctrl.serial|0);
ball.SetTag("sb_ctrl", cser);
// Kickoff one-step slide via timer; timerID 42 is arbitrary but within 0..100
ball.StartTimer(KICK_SLIDE_MS, 42, true);
}
function onTimer(timerObj, timerID)
{
if ((timerID|0) !== 42) return;
if (!ValidateObject(timerObj) || !timerObj.isItem || timerObj.GetTag(TAG_IS_BALL) != "1") return;
var dx = _toInt(timerObj.GetTag("sb_dx"));
var dy = _toInt(timerObj.GetTag("sb_dy"));
var steps = _toInt(timerObj.GetTag("sb_steps"));
var ctrlSer = _toInt(timerObj.GetTag("sb_ctrl"));
var ctrl = _itemFromSer(ctrlSer) || _ctrlFindNear(timerObj, null);
if (steps <= 0) {
timerObj.SetTag("sb_dx", null); timerObj.SetTag("sb_dy", null); timerObj.SetTag("sb_steps", null); timerObj.SetTag("sb_ctrl", null);
return;
}
// Move one tile
var bx = timerObj.x + dx;
var by = timerObj.y + dy;
var bz = timerObj.z;
timerObj.Teleport(bx, by, bz, 0);
timerObj.Refresh();
steps -= 1; timerObj.SetTag("sb_steps", steps);
// Goal check using controller-stored goal coordinates
var scoredTeam = null;
if (ValidateObject(ctrl)) {
var sX = _getTagInt(ctrl, "silverGoalX", -32768), sY = _getTagInt(ctrl, "silverGoalY", -32768);
var gX = _getTagInt(ctrl, "goldGoalX", -32768), gY = _getTagInt(ctrl, "goldGoalY", -32768);
if (bx === sX && by === sY) scoredTeam = "silver";
else if (bx === gX && by === gY) scoredTeam = "gold";
}
if (scoredTeam) {
var key = (scoredTeam === "silver") ? "silverScore" : "goldScore";
var newScore = _getTagInt(ctrl, key, 0) + 1; _setTag(ctrl, key, newScore);
var msg = "GOAL for " + scoredTeam.toUpperCase() + "! Score: Silver " + _getTagInt(ctrl, "silverScore", 0) + " - Gold " + _getTagInt(ctrl, "goldScore", 0);
BroadcastMessage(msg);
// Win check
var maxPts = _getTagInt(ctrl, "maxPoints", 5)|0;
if (newScore >= maxPts && maxPts > 0) {
_setTag(ctrl, "matchOver", 1);
BroadcastMessage("SKULLBALL: "+scoredTeam.toUpperCase()+" WINS! (First to "+maxPts+")");
}
// Reset ball to center
var cx = _getTagInt(ctrl, "centerX", timerObj.x);
var cy = _getTagInt(ctrl, "centerY", timerObj.y);
var cz = _getTagInt(ctrl, "centerZ", timerObj.z);
timerObj.Teleport(cx, cy, cz, 0);
timerObj.Refresh();
// Optional freeze kickoff
if (_toInt(ctrl.GetTag("freezePlayers"))) {
var ms = _getTagInt(ctrl, "freezeTimeMs", CTRL_DEFAULTS.freezeTimeMs);
AreaCharacterFunction(function(c){ if (!c.npc) _freeze(c, ms); return 0; }, timerObj, 15, null);
}
// Award nearby spectators/players (12 tiles around the ball at center)
var recipients = [];
AreaCharacterFunction(function(c){ if (!c.npc && c.visible && c.InRange(timerObj, 12)) recipients.push(c); return 0; }, timerObj, 12, null);
_awardTeam(ctrl, scoredTeam, recipients);
// Clear slide state and stop
timerObj.SetTag("sb_dx", null); timerObj.SetTag("sb_dy", null); timerObj.SetTag("sb_steps", null); timerObj.SetTag("sb_ctrl", null);
return;
}
// Continue sliding if steps remain
if (steps > 0) timerObj.StartTimer(KICK_SLIDE_MS, 42, true);
else { timerObj.SetTag("sb_dx", null); timerObj.SetTag("sb_dy", null); timerObj.SetTag("sb_steps", null); timerObj.SetTag("sb_ctrl", null); }
}
function _dirStep(from, to) {
var dx = 0, dy = 0;
if (to.x > from.x) dx = 1; else if (to.x < from.x) dx = -1;
if (to.y > from.y) dy = 1; else if (to.y < from.y) dy = -1;
return { dx: dx, dy: dy };
}
// Map character facing (.dir) to a movement step
// 0=N,1=NE,2=E,3=SE,4=S,5=SW,6=W,7=NW
function _dirToStep(d)
{
d = d|0;
switch (d & 7) {
case 0: return { dx: 0, dy: -1 };
case 1: return { dx: 1, dy: -1 };
case 2: return { dx: 1, dy: 0 };
case 3: return { dx: 1, dy: 1 };
case 4: return { dx: 0, dy: 1 };
case 5: return { dx: -1, dy: 1 };
case 6: return { dx: -1, dy: 0 };
case 7: return { dx: -1, dy: -1 };
default: return { dx: 0, dy: 0 };
}
}
// ---------------------------------
// Event: CommandRegistration / GM
// ---------------------------------
// ---------------------------------
// CommandRegistration / GM command
// ---------------------------------
function CommandRegistration() { RegisterCommand("skullball", 9 /* GM */, true); }
/** @type {(socket: Socket, cmdString: string) => void} */
function command_SKULLBALL(socket, cmdString)
{
var p = socket.currentChar; if (!ValidateObject(p)) return;
// Parse args
var parts = ((cmdString || "").trim()).split(/\s+/);
var sub = (parts.shift() || "").toLowerCase();
switch (sub)
{
case "":
case "help":
socket.SysMessage("Skullball cmds:");
socket.SysMessage(" 'skullball setup - create/init controller near you");
socket.SysMessage(" 'skullball ball - place ball (links to controller)");
socket.SysMessage(" 'skullball goal silver - place Silver goal at your feet");
socket.SysMessage(" 'skullball goal gold - place Gold goal at your feet");
socket.SysMessage(" 'skullball center - set ball reset point to your tile");
socket.SysMessage(" 'skullball score - show scores + max");
socket.SysMessage(" 'skullball max <N> - set target points to win");
socket.SysMessage(" 'skullball freeze 1|0 - toggle kickoff freeze");
socket.SysMessage(" 'skullball reward 1|0 - toggle simple rewards");
socket.SysMessage(" 'skullball reset - reset scores and end-of-match flag");
return;
case "setup": {
_ctrlEnsure(p);
socket.SysMessage("Skullball: controller initialized here.");
return;
}
case "ball": {
_createBall(p);
return;
}
case "goal": {
var team = (parts.shift() || "").toLowerCase();
if (team !== "silver" && team !== "gold") {
socket.SysMessage("Usage: 'skullball goal silver|gold");
return;
}
_createGoal(p, team);
return;
}
case "center": {
var c = _ctrlEnsure(p);
_setTag(c, "centerX", p.x);
_setTag(c, "centerY", p.y);
_setTag(c, "centerZ", p.z);
socket.SysMessage("Skullball: center reset point updated.");
return;
}
case "score": {
var ctrl = _ctrlFindNear(p, socket);
if (!ValidateObject(ctrl)) { socket.SysMessage("No Skullball controller nearby."); return; }
socket.SysMessage(
"Skullball Score => Silver " + _getTagInt(ctrl, "silverScore", 0) +
" | Gold " + _getTagInt(ctrl, "goldScore", 0) +
" | Max " + _getTagInt(ctrl, "maxPoints", 5)
);
return;
}
case "max": {
var m = _toInt(parts.shift()); if (m <= 0) m = 1;
var ct = _ctrlEnsure(p);
_setTag(ct, "maxPoints", m);
socket.SysMessage("Skullball: max points set to " + m + ".");
return;
}
case "freeze": {
var on = _toInt(parts.shift());
var ct = _ctrlEnsure(p);
_setTag(ct, "freezePlayers", on ? 1 : 0);
socket.SysMessage("Skullball: freeze-on-kickoff = " + (on ? "ON" : "OFF") + ".");
return;
}
case "reward": {
var on = _toInt(parts.shift());
var ct = _ctrlEnsure(p);
_setTag(ct, "giveReward", on ? 1 : 0);
socket.SysMessage("Skullball: rewards = " + (on ? "ON" : "OFF") + ".");
return;
}
case "reset": {
var ct = _ctrlFindNear(p, socket);
if (!ValidateObject(ct)) { socket.SysMessage("No Skullball controller nearby to reset."); return; }
_setTag(ct, "silverScore", 0);
_setTag(ct, "goldScore", 0);
_setTag(ct, "matchOver", 0);
socket.SysMessage("Skullball: scores cleared; match unlocked.");
return;
}
default:
socket.SysMessage("Unknown subcommand. Try 'skullball help");
return;
}
}
// ---------------------------------
// Event: onCollide – ball/goal/gates
// ---------------------------------
function onCollide(targSock, pColliding, objCollidedWith)
{
if (!ValidateObject(pColliding) || !pColliding.isChar) return false;
if (!ValidateObject(objCollidedWith) || !objCollidedWith.isItem) return false;
var sec = objCollidedWith.sectionID || "";
var isBall = objCollidedWith.GetTag(TAG_IS_BALL) == "1";
var goalTag = objCollidedWith.GetTag(TAG_IS_GOAL);
// Kick the ball when we bump into it
if (isBall)
{
var ctrl = _ctrlFromBall(objCollidedWith) || _ctrlFindNear(objCollidedWith, targSock);
if (!ValidateObject(ctrl)) {
if (targSock) targSock.SysMessage("Skullball: place a controller with 'skullball setup first.");
return false; // block default handling
}
if ((_getTagInt(ctrl, "matchOver", 0) | 0) === 1) {
if (targSock) targSock.SysMessage("Match over use 'skullball reset.");
return false;
}
// Use the player's facing direction for kick vector
var step = _dirToStep(pColliding.direction | 0);
// Fallback: if direction is unknown, nudge based on relative position
if (step.dx === 0 && step.dy === 0) {
step = _dirStep(pColliding, objCollidedWith);
if (step.dx === 0 && step.dy === 0) step.dx = 1; // tiny east nudge
}
_kickBall(pColliding, objCollidedWith, ctrl, step.dx, step.dy);
return false; // we handled it; suppress default collision
}
// Enter gate -> move near center, 2 tiles north of center
if (sec === "skullball_gate_enter")
{
var ctrl2 = _ctrlFromBall(objCollidedWith) || _ctrlFindNear(objCollidedWith, targSock) || _ctrlEnsure(pColliding);
var cx = _getTagInt(ctrl2, "centerX", objCollidedWith.x);
var cy = _getTagInt(ctrl2, "centerY", objCollidedWith.y);
var cz = _getTagInt(ctrl2, "centerZ", objCollidedWith.z);
pColliding.Teleport(cx, cy - 2, cz, 0);
return false; // handled
}
// Exit gate -> bump +5 east
if (sec === "skullball_gate_exit")
{
pColliding.Teleport(objCollidedWith.x + 5, objCollidedWith.y, objCollidedWith.z, 0);
return false; // handled
}
// Touching goals: no-op; scoring happens when the ball hits the goal
if (goalTag) return false;
return false;
}
// Constants / defaults
// ---------------------
var SCRIPT_ID = 7555;
var TAG_CTRL = "skullball_ctrl"; // tag placed on controller item (value "1")
var TAG_IS_GOAL = "skullball_goal"; // tag placed on goal mouth items: value "silver" or "gold"
var TAG_IS_BALL = "skullball_ball"; // tag placed on ball item: value "1"
var CTRL_DEFAULTS = {
giveReward: 1, // 1/0
freezePlayers: 1, // 1/0
freezeTimeMs: 3000, // milliseconds
rewardMinTeam: 1, // minimum members per team to award
showDateOnReward: 1, // 1/0
showNameOnReward: 1, // 1/0
showTeamSizeOnReward: 1 // 1/0
};
var HUE_SILVER = 1150;
var HUE_GOLD = 1161;
var HUE_REF = 1153;
var KICK_MAX_STEPS = 2; // how many tiles a kick can push the ball
var KICK_SLIDE_MS = 75; // delay between slide steps (visual nicety)
// ---------------------
// Helpers
// ---------------------
function _toInt(v) { var n = parseInt(v, 10); return isNaN(n) ? 0 : n; }
function _toBool01(v) { return (v|0) ? 1 : 0; }
function _getTagInt(o, k, d) {
var raw = o.GetTag(k);
if (raw === null || raw === undefined || raw === "") return (d|0);
var n = parseInt(raw, 10);
return isNaN(n) ? (d|0) : n;
}
function _getTagStr(o, k, d) { var v = o.GetTag(k); return (v===null || v===undefined || v === "") ? (d||"") : (""+v); }
function _setTag(o, k, v) { o.SetTag(k, v); }
function _setTempTag(o, k, v) { o.SetTempTag(k, v); }
function _clrTag(o, k) { o.DelTag(k); }
// Safe serial -> item resolver (handles shard differences)
function _itemFromSer(ser)
{
ser |= 0; if (ser <= 0) return null;
if (typeof CalcItemFromSer === "function") return CalcItemFromSer(ser);
if (typeof CalcItemBySerial === "function") return CalcItemBySerial(ser);
return null;
}
function AFN_FindCtrl(srcChar, trgItem, pSock)
{
if (trgItem.GetTag(TAG_CTRL) == "1") { pSock.tempInt1 = trgItem.serial; return true; }
return false;
}
function _ctrlFindNear(pivot, sock)
{
var useSock = sock || (pivot && pivot.socket) || null;
if (!useSock) return null;
useSock.tempInt1 = 0;
AreaItemFunction("AFN_FindCtrl", pivot, 20, useSock);
var ser = useSock.tempInt1|0; if (ser <= 0) return null;
var found = CalcItemFromSer ? CalcItemFromSer(ser) : null;
return found;
}
function _ctrlEnsure(pivot) {
var c = _ctrlFindNear(pivot, pivot.socket);
if (!ValidateObject(c)) {
c = CreateBlankItem(pivot.socket, pivot, 1, "Skullball Controller", 0x1F14, 0x0000, "ITEM", false); // invisible controller
c.visible = 0; c.movable = 0; c.name = "Skullball Controller"; c.hue = 0x0000;
_setTag(c, TAG_CTRL, "1");
// defaults
_setTag(c, "silverScore", 0);
_setTag(c, "goldScore", 0);
_setTag(c, "freezePlayers", CTRL_DEFAULTS.freezePlayers);
_setTag(c, "freezeTimeMs", CTRL_DEFAULTS.freezeTimeMs);
_setTag(c, "giveReward", CTRL_DEFAULTS.giveReward);
_setTag(c, "rewardMinTeam", CTRL_DEFAULTS.rewardMinTeam);
_setTag(c, "showDateOnReward", CTRL_DEFAULTS.showDateOnReward);
_setTag(c, "showNameOnReward", CTRL_DEFAULTS.showNameOnReward);
_setTag(c, "showTeamSizeOnReward", CTRL_DEFAULTS.showTeamSizeOnReward);
_setTag(c, "maxPoints", CTRL_DEFAULTS.maxPoints||5);
_setTag(c, "matchOver", 0);
}
// ensure defaults exist on old controllers
if (c.GetTag("maxPoints") === null || c.GetTag("maxPoints") === undefined || c.GetTag("maxPoints") === "")
_setTag(c, "maxPoints", CTRL_DEFAULTS.maxPoints||5);
if (c.GetTag("matchOver") === null || c.GetTag("matchOver") === undefined || c.GetTag("matchOver") === "")
_setTag(c, "matchOver", 0);
return c;
}
function _ctrlFromBall(ball)
{
if (!ValidateObject(ball)) return null;
var s = _toInt(ball.GetTag("ctrlSer"));
if (s > 0) { var c = _itemFromSer(s); if (ValidateObject(c)) return c; }
var sock = (ball.container && ball.container.socket) || null;
if (sock) {
sock.tempInt1 = 0; AreaItemFunction("AFN_FindCtrl", ball, 40, sock);
var ser = sock.tempInt1|0; if (ser > 0) { ball.SetTag("ctrlSer", ser); return _itemFromSer(ser); }
}
return null;
}
function _createBall(pUser) {
var c = _ctrlEnsure(pUser);
var ball = CreateBlankItem(pUser.socket, pUser, 1, "Skullball", 6880, 1152, "ITEM", false); // skull
ball.name = "Skullball";
ball.movable = 0;
_setTag(ball, TAG_IS_BALL, "1");
ball.AddScriptTrigger(SCRIPT_ID);
ball.Refresh();
_setTag(c, "ballSerial", ball.serial);
ball.SetTag("ctrlSer", c.serial|0);
_setTag(c, "centerX", pUser.x);
_setTag(c, "centerY", pUser.y);
_setTag(c, "centerZ", pUser.z);
_setTag(c, "world", pUser.worldNumber);
_setTag(c, "instance", pUser.instanceID);
pUser.SysMessage("Skullball placed. This tile set as center.");
return ball;
}
function _createGoal(pUser, team) {
var art = 0x1B73; // skull pike as a placeholder goal marker
var i = CreateBlankItem(pUser.socket, pUser, 1, (team === "silver" ? "Silver" : "Gold") + " Goal", art, (team === "silver" ? HUE_SILVER : HUE_GOLD), "ITEM", false);
i.name = (team === "silver" ? "Silver" : "Gold") + " Goal";
i.hue = (team === "silver" ? HUE_SILVER : HUE_GOLD);
i.movable = 0;
_setTag(i, TAG_IS_GOAL, team);
i.AddScriptTrigger(SCRIPT_ID);
i.Refresh();
var c = _ctrlEnsure(pUser);
_setTag(c, team+"GoalX", pUser.x);
_setTag(c, team+"GoalY", pUser.y);
_setTag(c, team+"GoalZ", pUser.z);
_setTag(c, team+"GoalSerial", i.serial);
pUser.SysMessage((team === "silver"?"Silver":"Gold") + " goal placed.");
return i;
}
function AFN_FindGoalAt(srcObj, trgItem, pSock)
{
var tx = pSock.tempInt2|0, ty = pSock.tempInt3|0;
if (trgItem.x === tx && trgItem.y === ty && trgItem.GetTag(TAG_IS_GOAL)) { pSock.tempInt1 = trgItem.serial; return true; }
return false;
}
function _findGoalAt(pivotItem, x, y, sock)
{
var useSock = sock || (pivotItem && pivotItem.container && pivotItem.container.socket) || null;
if (!useSock || !ValidateObject(pivotItem)) return null;
useSock.tempInt1 = 0; useSock.tempInt2 = x|0; useSock.tempInt3 = y|0;
AreaItemFunction("AFN_FindGoalAt", pivotItem, 0, useSock);
var ser = useSock.tempInt1|0; if (ser <= 0) return null;
return _itemFromSer(ser);
}
function _awardTeam(ctrl, team, players) {
if (!_toInt(ctrl.GetTag("giveReward"))) return;
var minTeam = _toInt(ctrl.GetTag("rewardMinTeam"));
if (!players || players.length < minTeam) return;
for (var i = 0; i < players.length; i++) {
var ch = players[i];
if (!ValidateObject(ch) || !ch.isChar || ch.npc) continue;
var pack = ch.pack;
if (!ValidateObject(pack)) continue;
var trophy = CreateBlankItem(ch.socket, ch, 1, "Skullball Trophy", 0x0E2D, (team==="silver"?HUE_SILVER:HUE_GOLD), "ITEM", true); // trophy to backpack
trophy.hue = (team=="silver"?HUE_SILVER:HUE_GOLD);
var parts = [ team.toUpperCase()+" SKULLBALL TROPHY" ];
if (_toInt(ctrl.GetTag("showNameOnReward"))) parts.push("- "+ch.name);
if (_toInt(ctrl.GetTag("showTeamSizeOnReward"))) parts.push("(Team "+players.length+")");
if (_toInt(ctrl.GetTag("showDateOnReward"))) parts.push("- "+new Date().toDateString());
trophy.name = parts.join(" ");
}
}
function _freeze(ch, ms) {
if (!ValidateObject(ch) || !ch.isChar) return;
ch.Freeze(_toInt(ms));
}
function _kickBall(pUser, ball, ctrl, dx, dy) {
if (!ValidateObject(ball)) return;
// Store slide state on the ball and start a timer that calls back into THIS script
ball.SetTag("sb_dx", (dx|0));
ball.SetTag("sb_dy", (dy|0));
ball.SetTag("sb_steps", (KICK_MAX_STEPS|0));
var cser = _toInt(ball.GetTag("ctrlSer"));
if (!cser && ValidateObject(ctrl)) cser = (ctrl.serial|0);
ball.SetTag("sb_ctrl", cser);
// Kickoff one-step slide via timer; timerID 42 is arbitrary but within 0..100
ball.StartTimer(KICK_SLIDE_MS, 42, true);
}
function onTimer(timerObj, timerID)
{
if ((timerID|0) !== 42) return;
if (!ValidateObject(timerObj) || !timerObj.isItem || timerObj.GetTag(TAG_IS_BALL) != "1") return;
var dx = _toInt(timerObj.GetTag("sb_dx"));
var dy = _toInt(timerObj.GetTag("sb_dy"));
var steps = _toInt(timerObj.GetTag("sb_steps"));
var ctrlSer = _toInt(timerObj.GetTag("sb_ctrl"));
var ctrl = _itemFromSer(ctrlSer) || _ctrlFindNear(timerObj, null);
if (steps <= 0) {
timerObj.SetTag("sb_dx", null); timerObj.SetTag("sb_dy", null); timerObj.SetTag("sb_steps", null); timerObj.SetTag("sb_ctrl", null);
return;
}
// Move one tile
var bx = timerObj.x + dx;
var by = timerObj.y + dy;
var bz = timerObj.z;
timerObj.Teleport(bx, by, bz, 0);
timerObj.Refresh();
steps -= 1; timerObj.SetTag("sb_steps", steps);
// Goal check using controller-stored goal coordinates
var scoredTeam = null;
if (ValidateObject(ctrl)) {
var sX = _getTagInt(ctrl, "silverGoalX", -32768), sY = _getTagInt(ctrl, "silverGoalY", -32768);
var gX = _getTagInt(ctrl, "goldGoalX", -32768), gY = _getTagInt(ctrl, "goldGoalY", -32768);
if (bx === sX && by === sY) scoredTeam = "silver";
else if (bx === gX && by === gY) scoredTeam = "gold";
}
if (scoredTeam) {
var key = (scoredTeam === "silver") ? "silverScore" : "goldScore";
var newScore = _getTagInt(ctrl, key, 0) + 1; _setTag(ctrl, key, newScore);
var msg = "GOAL for " + scoredTeam.toUpperCase() + "! Score: Silver " + _getTagInt(ctrl, "silverScore", 0) + " - Gold " + _getTagInt(ctrl, "goldScore", 0);
BroadcastMessage(msg);
// Win check
var maxPts = _getTagInt(ctrl, "maxPoints", 5)|0;
if (newScore >= maxPts && maxPts > 0) {
_setTag(ctrl, "matchOver", 1);
BroadcastMessage("SKULLBALL: "+scoredTeam.toUpperCase()+" WINS! (First to "+maxPts+")");
}
// Reset ball to center
var cx = _getTagInt(ctrl, "centerX", timerObj.x);
var cy = _getTagInt(ctrl, "centerY", timerObj.y);
var cz = _getTagInt(ctrl, "centerZ", timerObj.z);
timerObj.Teleport(cx, cy, cz, 0);
timerObj.Refresh();
// Optional freeze kickoff
if (_toInt(ctrl.GetTag("freezePlayers"))) {
var ms = _getTagInt(ctrl, "freezeTimeMs", CTRL_DEFAULTS.freezeTimeMs);
AreaCharacterFunction(function(c){ if (!c.npc) _freeze(c, ms); return 0; }, timerObj, 15, null);
}
// Award nearby spectators/players (12 tiles around the ball at center)
var recipients = [];
AreaCharacterFunction(function(c){ if (!c.npc && c.visible && c.InRange(timerObj, 12)) recipients.push(c); return 0; }, timerObj, 12, null);
_awardTeam(ctrl, scoredTeam, recipients);
// Clear slide state and stop
timerObj.SetTag("sb_dx", null); timerObj.SetTag("sb_dy", null); timerObj.SetTag("sb_steps", null); timerObj.SetTag("sb_ctrl", null);
return;
}
// Continue sliding if steps remain
if (steps > 0) timerObj.StartTimer(KICK_SLIDE_MS, 42, true);
else { timerObj.SetTag("sb_dx", null); timerObj.SetTag("sb_dy", null); timerObj.SetTag("sb_steps", null); timerObj.SetTag("sb_ctrl", null); }
}
function _dirStep(from, to) {
var dx = 0, dy = 0;
if (to.x > from.x) dx = 1; else if (to.x < from.x) dx = -1;
if (to.y > from.y) dy = 1; else if (to.y < from.y) dy = -1;
return { dx: dx, dy: dy };
}
// Map character facing (.dir) to a movement step
// 0=N,1=NE,2=E,3=SE,4=S,5=SW,6=W,7=NW
function _dirToStep(d)
{
d = d|0;
switch (d & 7) {
case 0: return { dx: 0, dy: -1 };
case 1: return { dx: 1, dy: -1 };
case 2: return { dx: 1, dy: 0 };
case 3: return { dx: 1, dy: 1 };
case 4: return { dx: 0, dy: 1 };
case 5: return { dx: -1, dy: 1 };
case 6: return { dx: -1, dy: 0 };
case 7: return { dx: -1, dy: -1 };
default: return { dx: 0, dy: 0 };
}
}
// ---------------------------------
// Event: CommandRegistration / GM
// ---------------------------------
// ---------------------------------
// CommandRegistration / GM command
// ---------------------------------
function CommandRegistration() { RegisterCommand("skullball", 9 /* GM */, true); }
/** @type {(socket: Socket, cmdString: string) => void} */
function command_SKULLBALL(socket, cmdString)
{
var p = socket.currentChar; if (!ValidateObject(p)) return;
// Parse args
var parts = ((cmdString || "").trim()).split(/\s+/);
var sub = (parts.shift() || "").toLowerCase();
switch (sub)
{
case "":
case "help":
socket.SysMessage("Skullball cmds:");
socket.SysMessage(" 'skullball setup - create/init controller near you");
socket.SysMessage(" 'skullball ball - place ball (links to controller)");
socket.SysMessage(" 'skullball goal silver - place Silver goal at your feet");
socket.SysMessage(" 'skullball goal gold - place Gold goal at your feet");
socket.SysMessage(" 'skullball center - set ball reset point to your tile");
socket.SysMessage(" 'skullball score - show scores + max");
socket.SysMessage(" 'skullball max <N> - set target points to win");
socket.SysMessage(" 'skullball freeze 1|0 - toggle kickoff freeze");
socket.SysMessage(" 'skullball reward 1|0 - toggle simple rewards");
socket.SysMessage(" 'skullball reset - reset scores and end-of-match flag");
return;
case "setup": {
_ctrlEnsure(p);
socket.SysMessage("Skullball: controller initialized here.");
return;
}
case "ball": {
_createBall(p);
return;
}
case "goal": {
var team = (parts.shift() || "").toLowerCase();
if (team !== "silver" && team !== "gold") {
socket.SysMessage("Usage: 'skullball goal silver|gold");
return;
}
_createGoal(p, team);
return;
}
case "center": {
var c = _ctrlEnsure(p);
_setTag(c, "centerX", p.x);
_setTag(c, "centerY", p.y);
_setTag(c, "centerZ", p.z);
socket.SysMessage("Skullball: center reset point updated.");
return;
}
case "score": {
var ctrl = _ctrlFindNear(p, socket);
if (!ValidateObject(ctrl)) { socket.SysMessage("No Skullball controller nearby."); return; }
socket.SysMessage(
"Skullball Score => Silver " + _getTagInt(ctrl, "silverScore", 0) +
" | Gold " + _getTagInt(ctrl, "goldScore", 0) +
" | Max " + _getTagInt(ctrl, "maxPoints", 5)
);
return;
}
case "max": {
var m = _toInt(parts.shift()); if (m <= 0) m = 1;
var ct = _ctrlEnsure(p);
_setTag(ct, "maxPoints", m);
socket.SysMessage("Skullball: max points set to " + m + ".");
return;
}
case "freeze": {
var on = _toInt(parts.shift());
var ct = _ctrlEnsure(p);
_setTag(ct, "freezePlayers", on ? 1 : 0);
socket.SysMessage("Skullball: freeze-on-kickoff = " + (on ? "ON" : "OFF") + ".");
return;
}
case "reward": {
var on = _toInt(parts.shift());
var ct = _ctrlEnsure(p);
_setTag(ct, "giveReward", on ? 1 : 0);
socket.SysMessage("Skullball: rewards = " + (on ? "ON" : "OFF") + ".");
return;
}
case "reset": {
var ct = _ctrlFindNear(p, socket);
if (!ValidateObject(ct)) { socket.SysMessage("No Skullball controller nearby to reset."); return; }
_setTag(ct, "silverScore", 0);
_setTag(ct, "goldScore", 0);
_setTag(ct, "matchOver", 0);
socket.SysMessage("Skullball: scores cleared; match unlocked.");
return;
}
default:
socket.SysMessage("Unknown subcommand. Try 'skullball help");
return;
}
}
// ---------------------------------
// Event: onCollide – ball/goal/gates
// ---------------------------------
function onCollide(targSock, pColliding, objCollidedWith)
{
if (!ValidateObject(pColliding) || !pColliding.isChar) return false;
if (!ValidateObject(objCollidedWith) || !objCollidedWith.isItem) return false;
var sec = objCollidedWith.sectionID || "";
var isBall = objCollidedWith.GetTag(TAG_IS_BALL) == "1";
var goalTag = objCollidedWith.GetTag(TAG_IS_GOAL);
// Kick the ball when we bump into it
if (isBall)
{
var ctrl = _ctrlFromBall(objCollidedWith) || _ctrlFindNear(objCollidedWith, targSock);
if (!ValidateObject(ctrl)) {
if (targSock) targSock.SysMessage("Skullball: place a controller with 'skullball setup first.");
return false; // block default handling
}
if ((_getTagInt(ctrl, "matchOver", 0) | 0) === 1) {
if (targSock) targSock.SysMessage("Match over use 'skullball reset.");
return false;
}
// Use the player's facing direction for kick vector
var step = _dirToStep(pColliding.direction | 0);
// Fallback: if direction is unknown, nudge based on relative position
if (step.dx === 0 && step.dy === 0) {
step = _dirStep(pColliding, objCollidedWith);
if (step.dx === 0 && step.dy === 0) step.dx = 1; // tiny east nudge
}
_kickBall(pColliding, objCollidedWith, ctrl, step.dx, step.dy);
return false; // we handled it; suppress default collision
}
// Enter gate -> move near center, 2 tiles north of center
if (sec === "skullball_gate_enter")
{
var ctrl2 = _ctrlFromBall(objCollidedWith) || _ctrlFindNear(objCollidedWith, targSock) || _ctrlEnsure(pColliding);
var cx = _getTagInt(ctrl2, "centerX", objCollidedWith.x);
var cy = _getTagInt(ctrl2, "centerY", objCollidedWith.y);
var cz = _getTagInt(ctrl2, "centerZ", objCollidedWith.z);
pColliding.Teleport(cx, cy - 2, cz, 0);
return false; // handled
}
// Exit gate -> bump +5 east
if (sec === "skullball_gate_exit")
{
pColliding.Teleport(objCollidedWith.x + 5, objCollidedWith.y, objCollidedWith.z, 0);
return false; // handled
}
// Touching goals: no-op; scoring happens when the ball hits the goal
if (goalTag) return false;
return false;
}