[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
}
{
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
}
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().
// ============================================================================
// 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().
// ============================================================================
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;
}
// 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;
}