Page 1 of 1

pirate capt and crew and boat

Posted: Mon Nov 03, 2025 1:11 am
by dragon slayer
[capt]
{
GET=basehuman
NAME=a pirate captain
ID=0x0190
EQUIPITEM=listobject13
HAIRCOLOR=15
EQUIPITEM=listobject14
COLORMATCHHAIR
EQUIPITEM=0x13b6
EQUIPITEM=listobject20
COLORLIST=11
EQUIPITEM=listobject31
COLORLIST=11
EQUIPITEM=listobject32
COLORLIST=11
EQUIPITEM=listobject33
COLORLIST=11
EQUIPITEM=listobject34
COLORLIST=11
EQUIPITEM=listobject36
COLORLIST=11
STR=86 100
DEX=66 100
INT=71 85
MAGICRESISTANCE=490 580
PARRYING=250 480
SWORDSMANSHIP=640 1000
TACTICS=650 880
WRESTLING=260 580
TOPEACE=605 5
NPCWANDER=3
FX2=10
FLAG=EVIL
NPCAI=2
NOTRAIN
SCRIPT=7520
}
you need two scripts

pirate_boat.js 7522=custom/pirate_boat.js
// ============================================================================
// FILE 3: js/npc/pirates/pirate_boat.js (ScriptID: 7522)
// ============================================================================


function onBoatTurn(iBoat, oldDir, newDir, iTiller)
{
if(oldDir != newDir){
if(RandomNumber(0, 100) < 40 && _isValid(iTiller)) iTiller.TextMessage("Yarr, capt'n! She's come about!");
}
else{
if(RandomNumber(0, 100) < 25 && _isValid(iTiller)) iTiller.TextMessage("Holding steady.");
}
return false; // allow other boat scripts too
}


function onTurnBoat(iBoat, oldDir, newDir, iTiller)
{
if(oldDir != newDir){
if(RandomNumber(0, 100) < 30 && _isValid(iTiller)) iTiller.TextMessage("Set the sails! Changing course!");
}
return true; // prevent duplicate chatter from other scripts; change to false if desired
}

// ============================================================================
// USAGE QUICKSTART
// 1) Save these as THREE files using the names/ScriptIDs above. Register script IDs in jse_fileassociations.scp if needed.
// 2) Set BOAT_SECTION to the house/boat section you want (e.g., largedragonboat) in the captain file.
// 3) [add pirate_captain near the ocean. Captain spawns ship + crew, hunts player boats.
// 4) Captain death triggers sink animation; crew may reinforce on damage.
// 5) Boat now speaks/reacts on turns via onBoatTurn/onTurnBoat using GetTiller().
// ============================================================================
you need this for capt
7520=custom/pirate_captain.js
// ============================================================================
// UOX3 – Pirate Ship Encounter (Captain + Crew + Boat Triggers)
// Runtime: SpiderMonkey 1.8.5 (ES5)
// Notes:
//  - THREE scripts. Save them as separate files:
//      1) js/npc/pirates/pirate_captain.js   (ScriptID: 7520)
//      2) js/npc/pirates/pirate_crew.js      (ScriptID: 7521)
//      3) js/npc/pirates/pirate_boat.js      (ScriptID: 7522)  <-- onBoatTurn/onTurnBoat
//  - Boat is spawned via CreateBaseMulti(MultiID). Set BOAT_MULTI_ID to your boat.
//  - Timers use p.StartTimer(ms, id, true) so callbacks land in the captain script.
//  - Character scanning now uses AreaCharacterFunction (not GetMobilesInRange).
//  - Item scanning uses AreaItemFunction to find nearby boats (your preference).
// ============================================================================

// ============================================================================
// Shared helpers (copy into ALL files that use them or keep in a shared include)
// ============================================================================
function _asInt(v) { v = parseInt(v, 10); return isNaN(v) ? 0 : v; }
function _rand(min, max) { return RandomNumber(min | 0, max | 0); } // inclusive
function _msgGM(pChar, txt) { if (pChar && pChar.TextMessage) pChar.TextMessage(txt); }

// 8-way direction helper (returns one of N,NE,E,SE,S,SW,W,NW)
function _dirTo(ax, ay, bx, by)
{
    var dx = bx - ax, dy = by - ay;
    if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) { return "N"; } // trivial
    var ang = Math.atan2(dy, dx);
    var deg = ang * 180 / Math.PI; if (deg < 0) deg += 360;
    if (deg >= 337.5 || deg < 22.5) return "E";
    if (deg < 67.5) return "NE";
    if (deg < 112.5) return "N";
    if (deg < 157.5) return "NW";
    if (deg < 202.5) return "W";
    if (deg < 247.5) return "SW";
    if (deg < 292.5) return "S";
    return "SE";
}

// Boat movement constants – tweak to match your UOX3 build (see docs)
// -1: Anchored | 0: Stop | 1..24: see docs
var MOVE_ANCHORED = -1;      // anchored, unable to move
var MOVE_STOP = 0;       // stop, don't move
var MOVE_FORWARD = 1;       // move forward
var MOVE_BACK = 2;       // move backward
var MOVE_LEFT = 3;       // move left
var MOVE_RIGHT = 4;       // move right
var MOVE_FWD_LEFT = 5;       // move forward-left
var MOVE_FWD_RGHT = 6;       // move forward-right
var MOVE_BACK_LEFT = 7;       // move backward-left
var MOVE_BACK_RGHT = 8;       // move backward-right
var MOVE_LEFT_S = 9;       // move left slowly
var MOVE_RIGHT_S = 10;      // move right slowly
var MOVE_FWD_S = 11;      // move forward slowly
var MOVE_BACK_S = 12;      // move backward slowly

// ============================================================================
// FILE 1: js/npc/pirates/pirate_captain.js (ScriptID: 7520)
// ============================================================================

var CAP_TIMER_AI = 1;      // AI think loop
var CAP_TIMER_SINK = 2;      // sinking animation
var CAP_TICK_AI_MS = 1500;   // 1.5s cadence
var CAP_SINK_T_MS = 1000;   // 1.0s per sink tick
var CAP_TIMER_RF = 3; // new
var BOAT_MULTI_ID = 22;   // <-- SET ME to your boat MultiID
var BOAT_NAME = "a pirate ship";

var CREW_DFN_SECTION = "m_pirate";
var CREW_SPAWN_COUNT = 5;

var CHASE_RANGE = 200;    // look this far for enemy boats
var STANDOFF_RANGE = 10;     // stop when close to target boat
var MAX_CREW_NEAR = 10;     // cap for reactive spawns
var REACTIVE_CHANCE = 0.25;   // 25% chance to spawn a deckhand on hit
var PLAYER_NEAR_RADIUS = 16;    // used by Pirate_CountPlayers
var REINFORCE_COOLDOWN_MS = 5000;

function _capGetBoat(p) { var ser = _asInt(p.GetTag("pirateBoat")); return (ser > 0) ? CalcItemFromSer(ser) : null; }
function _capSetBoat(p, i) { if (ValidateObject(i)) p.SetTag("pirateBoat", "" + i.serial); }

function _capSayRandom(p)
{
    var lines = [
        "Yarr, trim the sails!", "Wind's with us!", "No quarter!",
        "Spyglass on the horizon!", "Hoist the colors!"
    ];
    if (Math.random() < 0.25) p.EmoteMessage("*" + lines[_rand(0, lines.length - 1)] + "*");
}

function _attachBoatScript(iBoat)
{
    //if(!ValidateObject(iBoat))
    //  return;

    iBoat.AddScriptTrigger(7522); // or: AddScriptTrigger(iBoat, "npc/pirates/pirate_boat.js");

}

function _capSpawnBoatAndCrew(p)
{
    if (ValidateObject(_capGetBoat(p))) return;
    var x = p.x, y = p.y, z = 0;
    var world = (p.worldnumber != null ? p.worldnumber : p.worldNum);
    var inst = (p.instanceID != null ? p.instanceID : p.instanceId);

    var iBoat = CreateHouse(BOAT_MULTI_ID, x, y - 1, z - 5, world, inst);
    if (!ValidateObject(iBoat) || !iBoat.IsBoat())
    {
        _msgGM(p, "[PirateCaptain] Failed to create boat from MultiID 0x" + (BOAT_MULTI_ID >>> 0).toString(16).toUpperCase() + ". Set BOAT_MULTI_ID to a valid boat multi.");
        return;
    }
    iBoat.name = BOAT_NAME;
    p.Teleport(iBoat);
    iBoat.SetTag("pirateCaptainSer", "" + p.serial);
    _capSetBoat(p, iBoat);

    _attachBoatScript(iBoat);

    // Spawn initial crew slightly above deck so they don't clip
    for(var i=0;i<CREW_SPAWN_COUNT;i++)
    {
        var c = SpawnNPC( CREW_DFN_SECTION, x, y-1, z - 3, world, inst );
        if(ValidateObject(c)){
        c.SetTag("pirateBoatSer", ""+iBoat.serial );
        c.SetTag("role","pirate_crew");
        }
    }
}

// ---- AreaCharacterFunction helpers ----
function Pirate_CountPlayers(srcObj, trgChar, pSock)
{
    if(!ValidateObject(trgChar) || !trgChar.isChar)
        return false;

    if (trgChar.npc)
        return false; // players only

    return true;                  // counts as 1
}
function Pirate_KillCrewOnBoat(srcObj, trgChar, pSock)
{
    if (!ValidateObject(trgChar) || !trgChar.isChar || !trgChar.npc) return false;
    if (trgChar.GetTag("pirateBoatSer") == ("" + srcObj.serial))
    {
        trgChar.Damage(9999, null, true, 0, 0, 0, 0, 0);
        return true; // counted as killed
    }
    return false;
}
function Pirate_CountCrewNearCaptain(srcObj, trgChar, pSock)
{
    if (!ValidateObject(trgChar) || !trgChar.isChar || !trgChar.npc) return false;
    return (trgChar.GetTag("role") === "pirate_crew");
}

// ---- Find enemy boat using AreaItemFunction + AreaCharacterFunction ----
function Pirate_FindBoatVisitor(srcChar, trgItem, pSock)
{
    if (!ValidateObject(trgItem) || !trgItem.IsBoat || !trgItem.IsBoat()) return false;
    if (srcChar.GetTag("pir_skipBoatSer") == ("" + trgItem.serial)) return false;

    var players = AreaCharacterFunction("Pirate_CountPlayers", trgItem, 16, pSock);
    if (players <= 0) return false;

    var dx = trgItem.x - srcChar.x, dy = trgItem.y - srcChar.y;
    var d2 = dx*dx + dy*dy;

    var bestD2 = parseFloat(srcChar.GetTag("pir_bestD2")) || 9e15;
    if (d2 < bestD2) {
        srcChar.SetTag("pir_bestD2", "" + d2);
        srcChar.SetTag("pir_bestBoatSer", "" + trgItem.serial);
    }
    return false;
}

function _findEnemyBoat(p, myBoat)
{
    p.SetTag("pir_bestD2", null);
    p.SetTag("pir_bestBoatSer", null);
    p.SetTag("pir_skipBoatSer", myBoat ? ("" + myBoat.serial) : "0");

    AreaItemFunction("Pirate_FindBoatVisitor", p, CHASE_RANGE, p.socket || null);

    var ser = parseInt(p.GetTag("pir_bestBoatSer"), 10) || 0;
    p.SetTag("pir_skipBoatSer", null);
    return ser > 0 ? CalcItemFromSer(ser) : null;
}

function _speakTiller(iBoat, line)
{
    var t = ValidateObject(iBoat) && iBoat.GetTiller ? iBoat.GetTiller() : null;
    if (ValidateObject(t) && Math.random() < 0.35)
    { // not every tick
        t.TextMessage(line);
    }
}

function _chooseMoveType(myBoat, enemyBoat)
{
    var dx = enemyBoat.x - myBoat.x, dy = enemyBoat.y - myBoat.y;
    var ax = Math.abs(dx), ay = Math.abs(dy);
    var slow = (ax + ay) <= 6;

    var mx = 0, my = 0;
    if (dx > 1) mx = 1; else if (dx < -1) mx = -1;
    if (dy > 1) my = 1; else if (dy < -1) my = -1;

    // Map (mx,my) to moveType
    if (mx === 0 && my === 0) return MOVE_STOP;
    if (mx === 1 && my === 0) return slow ? MOVE_RIGHT_S : MOVE_RIGHT;
    if (mx === -1 && my === 0) return slow ? MOVE_LEFT_S  : MOVE_LEFT;
    if (mx === 0 && my === 1) return slow ? MOVE_FWD_S   : MOVE_FORWARD;     // “forward” = up/negative y/east/west isn’t consistent—use your shard’s notion
    if (mx === 0 && my === -1) return slow ? MOVE_BACK_S  : MOVE_BACK;
    if (mx === 1 && my === 1)  return slow ? MOVE_FWD_RGHT : MOVE_FWD_RGHT;
    if (mx === -1 && my === 1) return slow ? MOVE_FWD_LEFT : MOVE_FWD_LEFT;
    if (mx === 1 && my === -1) return MOVE_BACK_RGHT;
    if (mx === -1 && my === -1) return MOVE_BACK_LEFT;
    return MOVE_FORWARD;
}

function _chaseBoat(p, myBoat, enemyBoat)
{
    if (!ValidateObject(myBoat) || !ValidateObject(enemyBoat)) return;

    // steer roughly with TurnBoat (keeps visuals nice)
    var dir = _dirTo(myBoat.x, myBoat.y, enemyBoat.x, enemyBoat.y);
    if (dir === "N") myBoat.TurnBoat(0);
    else if (dir === "NE" || dir === "E") myBoat.TurnBoat(2);
    else if (dir === "NW" || dir === "W") myBoat.TurnBoat(1);
    else if (dir === "S") myBoat.TurnBoat(3);

    var dx = enemyBoat.x - myBoat.x, dy = enemyBoat.y - myBoat.y;
    var close = (Math.abs(dx) + Math.abs(dy)) <= STANDOFF_RANGE;

    myBoat.moveType = close ? MOVE_ANCHORED : _chooseMoveType(myBoat, enemyBoat);
}

function _killCrewOnBoat(iBoat)
{
    if (!ValidateObject(iBoat)) return 0;
    return AreaCharacterFunction("Pirate_KillCrewOnBoat", iBoat, 16, null) | 0;
}

// ---------------- Captain events ----------------
function onCreateDFN(objMade, objType)
{
    // 1 = Character per UOX3 docs
    if (objType == 1 && ValidateObject(objMade))
    {
        objMade.title = "[Captain]";
        objMade.skills.archery = 850;     // 85.0
        objMade.skills.tactics = 875;     // 87.5
        objMade.skills.swords = 875;
        objMade.skills.macing = 875;
        objMade.skills.fencing = 975;     // 97.5
        objMade.skills.wrestling = 375;   // 37.5
        objMade.skills.resistingmagic = 675;
        objMade.karma = -5000; objMade.fame = 5000;
        objMade.SetTag("role", "pirate_captain");

        // Start AI loop
        objMade.StartTimer(CAP_TICK_AI_MS, CAP_TIMER_AI, true);
    }
}

function onTimer(p, tid)
{
    if (!p || !p.isChar) return false;

    if (tid === CAP_TIMER_AI)
    {
        _capSayRandom(p);

        var boat = _capGetBoat(p);

        if (!ValidateObject(boat))
            _capSpawnBoatAndCrew(p);
        boat = _capGetBoat(p);

        if (!ValidateObject(boat))
        {
            p.StartTimer(CAP_TICK_AI_MS, CAP_TIMER_AI, true);
            return true;
        }

        var enemyBoat = _findEnemyBoat(p, boat);
        if (!ValidateObject(enemyBoat))
        {
            boat.moveType = MOVE_ANCHORED;
            p.StartTimer(CAP_TICK_AI_MS, CAP_TIMER_AI, true);
            return true;
        }

        _chaseBoat(p, boat, enemyBoat);

        p.StartTimer(CAP_TICK_AI_MS, CAP_TIMER_AI, true);
        return true;
    }

    if (tid === CAP_TIMER_SINK)
    {
        var count = _asInt(p.GetTag("sinkCount"));
        var boat = _capGetBoat(p);
        if (!ValidateObject(boat)) { return false; }

        if (count === 4) { _killCrewOnBoat(boat); }
        if (count >= 15) { DeleteObject(boat); p.DelTag("sinkCount"); return false; }

        if (count < 5) { boat.SetLocation(boat.x, boat.y, boat.z - 1); }
        else { boat.SetLocation(boat.x, boat.y, boat.z - 3); }

        if (count >= 15)
        {
            if (ValidateObject(boat))
            {
                boat.moveType = MOVE_STOP; DeleteObject(boat);
            }
            p.SetTag("pirateBoat",null );
            p.SetTag("sinkCount",null );
            return false;
        }

        p.SetTag("sinkCount", "" + (count + 1));
        p.StartTimer(CAP_SINK_T_MS, CAP_TIMER_SINK, true);
        return true;
    }

    if (tid === CAP_TIMER_RF)
    {
        p.SetTag("rfCD", null);
        return true;
    }
    return false;
}

function onDeath(p, killer)
{
    var boat = _capGetBoat(p);
    if (ValidateObject(boat))
    {
        p.SetTag("sinkCount", "0");
        p.StartTimer(CAP_SINK_T_MS, CAP_TIMER_SINK, true);
    }
    return false;
}

function onDelete(p)
{
    var boat = _capGetBoat(p);
    if (ValidateObject(boat))
    {
        p.SetTag("sinkCount", "0");
        p.StartTimer(CAP_SINK_T_MS, CAP_TIMER_SINK, true);
    }
    return false;
}

function onDamage(p, attacker, amount, dtype, hloc, skill)
{
    if (!p || !attacker || !attacker.isChar) return false;

    var near = AreaCharacterFunction("Pirate_CountCrewNearCaptain", p, 10, p.socket || null) | 0;
    if (near < MAX_CREW_NEAR && Math.random() <= REACTIVE_CHANCE && p.GetTag("rfCD") != "1")
    {
        p.SetTag("rfCD","1");
        p.StartTimer(5000, CAP_TIMER_RF, true); // 5s cooldown

        var world = attacker.worldnumber != null ? attacker.worldnumber : attacker.worldNum;
        var inst  = attacker.instanceID != null ? attacker.instanceID : attacker.instanceId;
        var nx = attacker.x + _rand(-1, 1), ny = attacker.y + _rand(-1, 1), nz = attacker.z;
        var c = SpawnNPC(CREW_DFN_SECTION, nx, ny, nz, world, inst);
        if (ValidateObject(c)) { c.SetTag("role","pirate_crew"); c.Fight(attacker); }
    }
    return false;
}