skullball game

Got any custom JavaScript additions/tweaks you think other people would like to see? Post 'em here!
Post Reply
dragon slayer
UOX3 Guru
Posts: 776
Joined: Thu Dec 21, 2006 7:37 am
Has thanked: 4 times
Been thanked: 26 times

skullball game

Post by dragon slayer »

/*
* 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
}
// ---------------------
// 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;
}
Post Reply