Skip to content

Commit 15ed190

Browse files
committed
[lua,sql,c++] Fomor Party Mixin with Phominua Aqueduct Setup
1 parent 5320278 commit 15ed190

18 files changed

+541
-34
lines changed

scripts/mixins/fomor_party.lua

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
-----------------------------------
2+
-- Fomor Party System
3+
-- Handles Fomor mob parties of two types:
4+
-- Patrol Parties: Leader with followers that path together
5+
-- Guard Parties: Stationary mobs that have shared respawn behavior
6+
-----------------------------------
7+
require('scripts/globals/follow')
8+
require('scripts/globals/mixins')
9+
-----------------------------------
10+
g_mixins = g_mixins or {}
11+
xi = xi or {}
12+
xi.mix = xi.mix or {}
13+
xi.mix.fomorParty = xi.mix.fomorParty or {}
14+
15+
-----------------------------------
16+
-- Party Definitions by Zone
17+
-----------------------------------
18+
local phomiunaID = zones[xi.zone.PHOMIUNA_AQUEDUCTS]
19+
20+
local zoneParties =
21+
{
22+
[xi.zone.PHOMIUNA_AQUEDUCTS] =
23+
{
24+
patrol =
25+
{
26+
-- Upstairs NE Room
27+
{ leader = phomiunaID.mob.FOMOR_RANGER[3], followers = 1 },
28+
{ leader = phomiunaID.mob.FOMOR_BARD[3], followers = 1 },
29+
{ leader = phomiunaID.mob.FOMOR_RED_MAGE[4], followers = 1 },
30+
{ leader = phomiunaID.mob.FOMOR_MONK[5], followers = 1 },
31+
-- Upstairs SE Room
32+
{ leader = phomiunaID.mob.FOMOR_RANGER[5], followers = 1 },
33+
{ leader = phomiunaID.mob.FOMOR_PALADIN[4], followers = 1 },
34+
{ leader = phomiunaID.mob.FOMOR_RED_MAGE[2], followers = 1 },
35+
{ leader = phomiunaID.mob.FOMOR_BLACK_MAGE[4], followers = 1 },
36+
-- Upstairs SW Room
37+
{ leader = phomiunaID.mob.FOMOR_BARD[4], followers = 1 },
38+
{ leader = phomiunaID.mob.FOMOR_THIEF[4], followers = 1 },
39+
{ leader = phomiunaID.mob.FOMOR_WARRIOR[5], followers = 1 },
40+
{ leader = phomiunaID.mob.FOMOR_RANGER[8], followers = 1 },
41+
-- Upstairs NW Room
42+
{ leader = phomiunaID.mob.FOMOR_BARD[6], followers = 1 },
43+
{ leader = phomiunaID.mob.FOMOR_THIEF[6], followers = 1 },
44+
{ leader = phomiunaID.mob.FOMOR_WARRIOR[3], followers = 1 },
45+
{ leader = phomiunaID.mob.FOMOR_SAMURAI[4], followers = 1 },
46+
-- Downstairs L-7 Party
47+
{ leader = phomiunaID.mob.FOMOR_WARRIOR[1], followers = 2 },
48+
-- Downstairs I-7 Party
49+
{ leader = phomiunaID.mob.FOMOR_DRAGOON[1], followers = 2 },
50+
},
51+
52+
guard =
53+
{
54+
-- Tres Duendes Room
55+
{ members = { phomiunaID.mob.FOMOR_SAMURAI[1], phomiunaID.mob.FOMOR_NINJA[2], phomiunaID.mob.FOMOR_RANGER[1], phomiunaID.mob.FOMOR_DARK_KNIGHT[1] } },
56+
-- H-8 Ladder
57+
{ members = { phomiunaID.mob.FOMOR_PALADIN[2], phomiunaID.mob.FOMOR_SAMURAI[2], phomiunaID.mob.FOMOR_RED_MAGE[1] } },
58+
-- C-8 Ladder
59+
{ members = { phomiunaID.mob.FOMOR_BLACK_MAGE[2], phomiunaID.mob.FOMOR_WARRIOR[2], phomiunaID.mob.FOMOR_DARK_KNIGHT[3] } },
60+
},
61+
},
62+
}
63+
64+
-----------------------------------
65+
-- Party Definition Helpers
66+
-----------------------------------
67+
local function getFomorParty(mobID, parties)
68+
if not parties then
69+
return nil, nil
70+
end
71+
72+
if parties.patrol then
73+
for _, party in ipairs(parties.patrol) do
74+
if
75+
party.leader and
76+
party.followers and
77+
mobID >= party.leader and
78+
mobID <= party.leader + party.followers
79+
then
80+
return party, 'patrol'
81+
end
82+
end
83+
end
84+
85+
if parties.guard then
86+
for _, party in ipairs(parties.guard) do
87+
if party.members then
88+
for _, memberID in ipairs(party.members) do
89+
if memberID == mobID then
90+
return party, 'guard'
91+
end
92+
end
93+
end
94+
end
95+
end
96+
97+
return nil, nil
98+
end
99+
100+
local function getPartyLeader(party)
101+
if party then
102+
if party.leader then
103+
return party.leader
104+
elseif party.members and #party.members > 0 then
105+
return party.members[1]
106+
end
107+
end
108+
109+
return nil
110+
end
111+
112+
-----------------------------------
113+
-- Set up party behavior on spawn
114+
-- Handles superlinking, guard/patrol behavior, and following
115+
-----------------------------------
116+
xi.mix.fomorParty.onPartySpawn = function(mob)
117+
local parties = zoneParties[mob:getZoneID()]
118+
local mobID = mob:getID()
119+
local party, behavior = getFomorParty(mobID, parties)
120+
121+
if not party then
122+
return
123+
end
124+
125+
local leaderID = getPartyLeader(party)
126+
if leaderID then
127+
mob:setMobMod(xi.mobMod.SUPERLINK, leaderID)
128+
end
129+
130+
-- Allow followers to respawn (respawn blocked on death until leader respawns)
131+
if
132+
behavior == 'patrol' and
133+
mobID == leaderID and
134+
party.followers
135+
then
136+
mob:setMobMod(xi.mobMod.LEADER, party.followers)
137+
138+
-- Spawn followers properly on server start
139+
-- Otherwise allow them to respawn normally
140+
for i = 1, party.followers do
141+
local followerID = leaderID + i
142+
local follower = GetMobByID(followerID)
143+
144+
if follower then
145+
local respawnTime = follower:getRespawnTime()
146+
147+
if respawnTime ~= 0 then
148+
DisallowRespawn(followerID, false)
149+
end
150+
end
151+
end
152+
end
153+
154+
-- Follower spawning logic for patrol parties
155+
-- Track distance to leader and start following
156+
if
157+
behavior == 'patrol' and
158+
leaderID and
159+
mobID > leaderID and
160+
party.followers
161+
then
162+
local distance = mobID - leaderID
163+
mob:setMobMod(xi.mobMod.LEADER, -distance)
164+
165+
local leader = GetMobByID(leaderID)
166+
if leader then
167+
xi.follow.follow(mob, leader)
168+
end
169+
end
170+
171+
-- Guard party: stationary groups that coordinate deaths
172+
if behavior == 'guard' and party.members then
173+
mob:setMobMod(xi.mobMod.ROAM_DISTANCE, 0)
174+
mob:setMobMod(xi.mobMod.ROAM_RESET_FACING, 1)
175+
mob:setRoamFlags(xi.roamFlag.SCRIPTED)
176+
end
177+
end
178+
179+
-----------------------------------
180+
-- Handle roaming behavior and follower cleanup
181+
-----------------------------------
182+
xi.mix.fomorParty.onPartyRoam = function(mob)
183+
local parties = zoneParties[mob:getZoneID()]
184+
local mobID = mob:getID()
185+
local party, behavior = getFomorParty(mobID, parties)
186+
187+
if not party then
188+
return
189+
end
190+
191+
-- Patrol party logic
192+
-- Followers despawn if leader is not spawned
193+
if behavior == 'patrol' then
194+
local leaderID = getPartyLeader(party)
195+
196+
if leaderID and mobID ~= leaderID then
197+
local leader = GetMobByID(leaderID)
198+
199+
-- If leader is not spawned, despawn this follower
200+
if leader and not leader:isSpawned() then
201+
DespawnMob(mobID)
202+
return
203+
end
204+
end
205+
end
206+
207+
-- Guard mobs should return to spawn position and maintain spawn rotation
208+
if behavior == 'guard' and party.members then
209+
local spawnPos = mob:getSpawnPos()
210+
local pos = mob:getPos()
211+
212+
-- If not at spawn position, path back to it
213+
if spawnPos.x ~= pos.x or spawnPos.z ~= pos.z then
214+
mob:pathThrough({ spawnPos.x, spawnPos.y, spawnPos.z })
215+
end
216+
217+
return
218+
end
219+
end
220+
221+
-----------------------------------
222+
-- Handle party member death
223+
-- Prevents members from respawning independently
224+
-----------------------------------
225+
xi.mix.fomorParty.onPartyDeath = function(mob)
226+
local parties = zoneParties[mob:getZoneID()]
227+
local mobID = mob:getID()
228+
local party, behavior = getFomorParty(mobID, parties)
229+
230+
if not party then
231+
return
232+
end
233+
234+
-- Patrol party respawn
235+
-- Disallow respawn unless leader of party
236+
if behavior == 'patrol' then
237+
local leaderID = getPartyLeader(party)
238+
if leaderID and mobID ~= leaderID then
239+
DisallowRespawn(mobID, true)
240+
end
241+
242+
-- Guard party respawn
243+
elseif behavior == 'guard' and party.members then
244+
-- Check if all other party members are dead
245+
local allDead = true
246+
for _, memberID in ipairs(party.members) do
247+
if memberID and memberID ~= mobID then
248+
local member = GetMobByID(memberID)
249+
if member and member:isAlive() then
250+
allDead = false
251+
break
252+
end
253+
end
254+
end
255+
256+
-- Last member of the party died - sync all members to this mob's respawn time
257+
if allDead then
258+
local respawnTime = mob:getRespawnTime()
259+
for _, memberID in ipairs(party.members) do
260+
local member = GetMobByID(memberID)
261+
if member then
262+
member:setRespawnTime(respawnTime)
263+
DisallowRespawn(memberID, false)
264+
end
265+
end
266+
267+
-- Some members are still alive, don't allow respawn
268+
else
269+
DisallowRespawn(mobID, true)
270+
end
271+
end
272+
end
273+
274+
g_mixins.fomor_party = function(mob)
275+
end
276+
277+
return g_mixins.fomor_party

scripts/zones/Phomiuna_Aqueducts/IDs.lua

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,22 @@ zones[xi.zone.PHOMIUNA_AQUEDUCTS] =
2828
},
2929
mob =
3030
{
31-
EBA = GetFirstID('Eba'),
32-
MAHISHA = GetFirstID('Mahisha'),
33-
TRES_DUENDES = GetFirstID('Tres_Duendes'),
31+
EBA = GetFirstID('Eba'),
32+
FOMOR_BARD = GetTableOfIDs('Fomor_Bard'),
33+
FOMOR_BLACK_MAGE = GetTableOfIDs('Fomor_Black_Mage'),
34+
FOMOR_DARK_KNIGHT = GetTableOfIDs('Fomor_Dark_Knight'),
35+
FOMOR_DRAGOON = GetTableOfIDs('Fomor_Dragoon'),
36+
FOMOR_MONK = GetTableOfIDs('Fomor_Monk'),
37+
FOMOR_NINJA = GetTableOfIDs('Fomor_Ninja'),
38+
FOMOR_PALADIN = GetTableOfIDs('Fomor_Paladin'),
39+
FOMOR_RANGER = GetTableOfIDs('Fomor_Ranger'),
40+
FOMOR_RED_MAGE = GetTableOfIDs('Fomor_Red_Mage'),
41+
FOMOR_SAMURAI = GetTableOfIDs('Fomor_Samurai'),
42+
FOMOR_SUMMONER = GetTableOfIDs('Fomor_Summoner'),
43+
FOMOR_THIEF = GetTableOfIDs('Fomor_Thief'),
44+
FOMOR_WARRIOR = GetTableOfIDs('Fomor_Warrior'),
45+
MAHISHA = GetFirstID('Mahisha'),
46+
TRES_DUENDES = GetFirstID('Tres_Duendes'),
3447
},
3548
npc =
3649
{

scripts/zones/Phomiuna_Aqueducts/mobs/Fomor_Bard.lua

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,28 @@
22
-- Area: Phomiuna_Aqueducts
33
-- Mob: Fomor Bard
44
-----------------------------------
5-
mixins = { require('scripts/mixins/fomor_hate') }
5+
mixins =
6+
{
7+
require('scripts/mixins/follow'),
8+
require('scripts/mixins/fomor_hate'),
9+
require('scripts/mixins/fomor_party'),
10+
}
611
-----------------------------------
712
---@type TMobEntity
813
local entity = {}
914

15+
entity.onMobSpawn = function(mob)
16+
xi.mix.fomorParty.onPartySpawn(mob)
17+
end
18+
19+
entity.onMobRoam = function(mob)
20+
xi.mix.fomorParty.onPartyRoam(mob)
21+
end
22+
1023
entity.onMobDeath = function(mob, player, optParams)
24+
if optParams.isKiller or optParams.noKiller then
25+
xi.mix.fomorParty.onPartyDeath(mob)
26+
end
1127
end
1228

1329
return entity

scripts/zones/Phomiuna_Aqueducts/mobs/Fomor_Beastmaster.lua

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,4 @@ entity.onMobInitialize = function(mob)
1111
xi.pet.setMobPet(mob, 1, 'Fomors_Bat')
1212
end
1313

14-
entity.onMobDeath = function(mob, player, optParams)
15-
end
16-
1714
return entity

scripts/zones/Phomiuna_Aqueducts/mobs/Fomor_Black_Mage.lua

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,28 @@
22
-- Area: Phomiuna_Aqueducts
33
-- Mob: Fomor Black Mage
44
-----------------------------------
5-
mixins = { require('scripts/mixins/fomor_hate') }
5+
mixins =
6+
{
7+
require('scripts/mixins/follow'),
8+
require('scripts/mixins/fomor_hate'),
9+
require('scripts/mixins/fomor_party'),
10+
}
611
-----------------------------------
712
---@type TMobEntity
813
local entity = {}
914

15+
entity.onMobSpawn = function(mob)
16+
xi.mix.fomorParty.onPartySpawn(mob)
17+
end
18+
19+
entity.onMobRoam = function(mob)
20+
xi.mix.fomorParty.onPartyRoam(mob)
21+
end
22+
1023
entity.onMobDeath = function(mob, player, optParams)
24+
if optParams.isKiller or optParams.noKiller then
25+
xi.mix.fomorParty.onPartyDeath(mob)
26+
end
1127
end
1228

1329
return entity

0 commit comments

Comments
 (0)