- Light, Darkness (I don't plan on doing these)
- Poison cloud (I can't find a way to cleanly merge CloudSpellComponents like the builtin spell does)
- Force Field (should be easy to implement, just haven't bothered)
- Meteor storm (needs a clean way to detect when the party is underwater, also I don't know quite how its spread works yet)
All of these should have identical effects to the builtin spells except where otherwise noted (fireburst, cause fear, heal).
Code: Select all
-- Builtin burst spell rules:
-- Try to target the square in front of the party.
-- If this square is solid, or is blocked by an ObstacleComponent that would
-- block the party and its parent GameObject does NOT have a component named
-- "health", target the party's square instead.
-- If the path between the party and this square is blocked by a ForceFieldComponent
-- or DoorComponent, target the party's square instead.
--
-- This function emulates this behaviour.
--
-- NOTE: The builtin poison_cloud spell is a little different: it can be cast
-- through sparse doors (but not non-sparse ones). This behaviour in the
-- original game is intended, and easy to replicate, but since it leads to
-- abuses I don't think anyone actually wants it, so I haven't implemented it.
function getBurstSpellTarget(x,y,direction,elevation)
local dx,dy = getForward(direction)
local nx = x+dx
local ny = y+dy
local obs = party.map:checkObstacle(party,direction)
if obs == "door" or obs == "wall" then
return party.level,x,y,direction,elevation
elseif obs == "obstacle" then
for e in party.map:entitiesAt(party.x,party.y) do
if e.elevation == party.elevation then
for _,c in e:componentIterator() do
if c:isEnabled() and c:getClass() == "ForceFieldComponent" then
return party.level,x,y,direction,elevation
end
end
end
end
for e in party.map:entitiesAt(nx,ny) do
if e.elevation == party.elevation and not e.health then
for _,c in e:componentIterator() do
if c:isEnabled() then
local cls = c:getClass()
if cls == "ForceFieldComponent" or cls == "ObstacleComponent" and c:getBlockParty() then
return party.level,x,y,direction,elevation
end
end
end
end
end
end
return party.level,nx,ny,direction,elevation
end
function doBurstSpell(champion,x,y,direction,elevation,skill,burst,basePower,noSkillMult)
local spl = spawn(burst,getBurstSpellTarget(x,y,direction,elevation))
local ord = champion:getOrdinal()
spl.tiledamager:setCastByChampion(ord)
spl.tiledamager:setAttackPower(basePower*(noSkillMult and 1 or 1+skill*0.2))
endInvisibility()
return spl
end
function getProjectileSpellLaunchPosition(champion,x,y,direction,elevation)
local ord = champion:getOrdinal()
local left = nil
for i = 1,4 do
if party.party:getChampion(i):getOrdinal() == ord then
left = i == 1 or i == 3
break
end
end
local wpos = party:getWorldPosition()
local dx = nil
local dz = nil
if party.facing == 0 then
dx = left and -0.1 or 0.1
dz = -1
elseif party.facing == 1 then
dz = left and 0.1 or -0.1
dx = -1
elseif party.facing == 2 then
dx = left and 0.1 or -0.1
dz = 1
else -- party.facing == 3
dz = left and -0.1 or 0.1
dx = 1
end
return party:getWorldPosition()+vec(dx,1.35,dz)
end
function doProjectileSpell(champion,x,y,direction,elevation,skill,projectile,basePower,noSkillMult)
local spl = spawn(projectile,party.level,party.x,party.y,party.facing,party.elevation)
spl.projectile:setIgnoreEntity(party)
local ord = champion:getOrdinal()
spl.projectile:setCastByChampion(ord)
spl.projectile:setAttackPower(basePower*(noSkillMult and 1 or 1+skill*0.2))
spl:setWorldPosition(getProjectileSpellLaunchPosition(champion,x,y,direction,elevation))
endInvisibility()
return spl
end
function endInvisibility()
for i=1,4 do
party.party:getChampion(i):removeCondition("invisibility")
end
end
function doElementalShield(shield)
for i=1,4 do
party.party:getChampion(i):removeCondition("fire_shield")
party.party:getChampion(i):removeCondition("frost_shield")
party.party:getChampion(i):removeCondition("poison_shield")
party.party:getChampion(i):removeCondition("shock_shield")
party.party:getChampion(i):setCondition(shield)
end
playSound("generic_spell")
end
function hasShamanStaff(champion)
return champion:getItem(ItemSlot.Weapon) and champion:getItem(ItemSlot.Weapon).go.name == "shaman_staff" or
champion:getItem(ItemSlot.OffHand) and champion:getItem(ItemSlot.OffHand).go.name == "shaman_staff"
end
function castShield(champion,x,y,direction,elevation,skill)
playSound("generic_spell")
local dur = 30+skill*10
champion:setConditionValue("protective_shield",30+skill*10)
end
function castDarkbolt(champion,x,y,direction,elevation,skill)
doProjectileSpell(champion,x,y,direction,elevation,skill,"dark_bolt",10,true)
end
function castFireburst(champion,x,y,direction,elevation,skill)
local spl = doBurstSpell(champion,x,y,direction,elevation,skill,"fireburst",10,true)
spl.tiledamager:setDamageFlags(DamageFlags.NoLingeringEffects)
if skill >= 3 and math.random() < 0.4 then
for e in party.map:entitiesAt(spl.x,spl.y) do
if e.elevation == party.elevation then
for _,c in e:componentIterator() do
-- NOTE: original spell doesn't have the isAlive() check,
-- I added it to fix the bug where dead monsters get burning
-- particles
if c:getClass() == "MonsterComponent" and c:isAlive() then
c:setCondition("burning")
if e.burning then
e.burning:setCausedByChampion(champion:getOrdinal())
-- A second MonsterComponent wouldn't matter
break
end
end
end
end
end
end
end
function castFireball(champion,x,y,direction,elevation,skill)
doProjectileSpell(champion,x,y,direction,elevation,skill,skill < 2 and "fireball_small" or skill < 3 and "fireball_medium" or "fireball_large",30)
end
function castFireShield(champion,x,y,direction,elevation,skill)
doElementalShield("fire_shield")
end
function castIceShards(champion,x,y,direction,elevation,skill)
local spl = doBurstSpell(champion,x,y,direction,elevation,skill,"ice_shards",18)
spl.iceshards:setRange(2+skill)
if not spl.tiledamager:isEnabled() then
playSound("spell_fizzle")
end
end
function castDispel(champion,x,y,direction,elevation,skill)
doProjectileSpell(champion,x,y,direction,elevation,skill,"dispel_projectile",25)
end
function castFrostbolt(champion,x,y,direction,elevation,skill)
doProjectileSpell(champion,x,y,direction,elevation,skill,"frostbolt_"..math.clamp(skill,1,5),15)
end
function castFrostShield(champion,x,y,direction,elevation,skill)
doElementalShield("frost_shield")
end
function castShock(champion,x,y,direction,elevation,skill)
doBurstSpell(champion,x,y,direction,elevation,skill,"shockburst",22).tiledamager:setDamageFlags(DamageFlags.NoLingeringEffects)
end
function castInvisibility(champion,x,y,direction,elevation,skill)
for i=1,4 do party.party:getChampion(i):setCondition("invisibility") end
playSound("generic_spell")
end
function castLightningBolt(champion,x,y,direction,elevation,skill)
doProjectileSpell(champion,x,y,direction,elevation,skill,skill > 1 and "lightning_bolt_greater" or "lightning_bolt",30)
end
function castShockShield(champion,x,y,direction,elevation,skill)
doElementalShield("shock_shield")
end
function castPoisonBolt(champion,x,y,direction,elevation,skill)
-- Yes, Shaman Staff turns 2 skill poison_bolt into poison_bolt_greater. This is true in the vanilla game too!
-- Try it!
if hasShamanStaff(champion) then
skill = skill+1
end
doProjectileSpell(champion,x,y,direction,elevation,skill,skill >= 3 and "poison_bolt_greater" or "poison_bolt",15)
end
function castPoisonShield(champion,x,y,direction,elevation,skill)
doElementalShield("poison_shield")
end
function castOpenDoor(champion,x,y,direction,elevation,skill)
-- attack power doesn't matter
doProjectileSpell(champion,x,y,direction,elevation,skill,"open_door",1,true)
end
function castDisintegrate(champion,x,y,direction,elevation,skill)
local nl,nx,ny,nf,ne = getBurstSpellTarget(x,y,direction,elevation)
for e in party.map:entitiesAt(nx,ny) do
if e.elevation == party.elevation then
for _,c in e:componentIterator() do
if c:getClass() == "MonsterComponent" and c:isAlive() then
c:die()
end
end
end
end
spawn("wizard_explosion",nl,nx,ny,nf,ne)
playSound("force_field_cast")
endInvisibility()
end
-- NOTE: builtin cause_fear will not attempt to affect monsters that are already
-- fleeing. The user scripting interface does not have access to whether a
-- monster is fleeing, so we can't do that here.
function castCauseFear(champion,x,y,direction,elevation,skill)
local nl,nx,ny,nf,ne = getBurstSpellTarget(x,y,direction,elevation)
for e in party.map:entitiesAt(nx,ny) do
if e.elevation == party.elevation then
for _,c in e:componentIterator() do
if c:getClass() == "MonsterComponent" and c:isAlive() and c.go.brain then
if math.random(0,100) > c.go.brain:getMorale() then
c.go.brain:startFleeing()
else
c:showDamageText("Resists","0xFFFFFF")
end
break
end
end
end
end
if party.x == nx and party.y == ny then
for i=1,4 do
local champ = party.party:getChampion(i)
if champ:isAlive() and (not champ:hasCondition("paralyzed")) and math.random() < 0.5 then
champ:setCondition("paralyzed")
end
end
end
local p = spawn("particle_system",nl,nx,ny,nf,ne)
p.particle:setParticleSystem("fear_cloud")
p.particle:setOffset(vec(0,1.5,0))
playSound("wand_fear")
endInvisibility()
end
function castHeal(champion,x,y,direction,elevation,skill)
for i=1,4 do
local champ = party.party:getChampion(i)
if champ:isAlive() then
champ:regainHealth(100)
champ:playHealingIndicator()
end
end
-- NOTE: builtin spell seems to have this inside the for loop, because
-- the sound plays 4 times if the party has 4 alive champions, which
-- is probably a bug.
playSound("heal_party")
end
Code: Select all
defineObject{
name = "party",
baseObject = "party",
components = {
{
class = "Party",
onInit = function(self)
self.go.spells:loadFile("mod_assets/spells_implementation.lua")
end,
},
{
class = "Script",
name = "spells",
},
},
}
defineSpell{
name = "fireball",
uiName = "Fireball",
gesture = 1236,
manaCost = 43,
skill = "fire_magic",
requirements = { "fire_magic", 3, "air_magic", 1 },
icon = 61,
spellIcon = 7,
description = "A flaming ball of fire shoots from your fingertips causing devastating damage to your foes.",
onCast = function(champion,x,y,direction,elevation,skill)
party.spells.castFireball(champion,x,y,direction,elevation,skill)
end,
}
If you don't want to do either of those then you should ignore this thread.