Here is the code I used for breakable doors in the
Dungeon Master Resource:
Code: Select all
-- Breakable doors. These doors will break open when attacked. Thanks to new
-- Grimrock 2 features, these work from both sides unlike the Grimrock 1
-- versions. They can also be opened and closed like unbreakable doors, until
-- they're broken!
-- When a breakable door is destroyed, if there is a ScriptComponent on either
-- side of the door named "script" with a function named "onDoorDestroyed",
-- that function will be called when the door is destroyed. It is called just
-- before the door GameObject itself is destroyed, and is passed the GameObject
-- as its first and only argument. The return value is ignored. The GameObject
-- will be destroyed immediately after finishing these calls, so don't bother
-- doing anything to the object inside them. The primary use of this feature is
-- to give you an easy way to remove connectors to the door when it is destroyed
-- (which will prevent the console warnings that you get for a connector being
-- unable to find its target). It's also well-suited to triggering some other
-- event when the party destroys the door, such as activating a trap or waking
-- up nearby monsters.
-- WARNING: When these doors are broken, the GameObject is DESTROYED.
-- WARNING: Do not use DoorComponent:open(), DoorComponent:close(),
-- DoorComponent:toggle(), or DoorComponent:setDoorState() on breakable doors.
-- Instead, use the equivalent ControllerComponent methods. On breakable
-- doors, use ControllerComponent:start() instead of
-- DoorComponent:setDoorState("open"), and ControllerComponent:stop() instead
-- of DoorComponent:setDoorState("closed").
-- The reason you should only use the ControllerComponent methods is that the
-- door's projectile collider needs to be disabled when the door is opened and
-- re-enabled when it is closed. The custom projectile collider is required to
-- detect ranged attacks against the breakable door.
--------------------------------------------------------------------------------
defineObject{
name = "dm_door_walltrigger",
components = {
{
class = "WallTrigger",
entityType = "any",
onActivate = function(self,entity)
local doorObject = findEntity(self.go.id:sub(7))
if doorObject then
doorObject.script.rangedHit(self,entity)
else
Console.warn(string.format("door for %s not found",self.go.id))
end
end,
},
{
class = "ProjectileCollider",
size = vec(2.5, 3, 0.1),
offset = vec(0,1.5,-0.1),
},
},
placement = "wall",
editorIcon = 72,
}
defineObject{
name = "dm_fakedoor_wood_stfr",
baseObject = "base_door",
components = {
{
class = "Model",
model = "mod_assets/dmcsb_pack/models/env/dm_door_wood.fbx",
},
{
class = "Model",
name = "frame",
model = "mod_assets/dmcsb_pack/models/env/dm_door_frame.fbx",
staticShadow = true,
},
{
class = "Door",
openVelocity = 1.3,
closeVelocity = 0,
closeAcceleration = -10,
killPillars = true,
--hitSound = "impact_blunt",
pullchainObject = "dm_door_button",
onAttackedByChampion = function(self, champion, weapon, attack, slot)
self.go.script.meleeHit(self,champion,weapon,attack,slot)
end,
},
{
class = "Script",
onInit = function(self)
self.setMaterial("wood")
end,
},
{
class = "Controller",
onInit = function(self)
local g = self.go
if not findEntity("owtrig"..self.go.id) then
-- Make a wall trigger on the other side.
local dx,dy = getForward(g.facing)
local nx = g.x+dx
local ny = g.y+dy
if (nx > -1 and nx < 32 and ny > -1 and ny < 32) then
-- if you replace "owtrig" with something else, update dm_door_walltrigger
-- and breakable_door.lua (sorry about this miserable hack)
spawn("dm_door_walltrigger",g.level,nx,ny,(g.facing+2)%4,g.elevation,"owtrig"..g.id)
end
end
-- This doesn't work if I do it in the
-- ScriptComponent for some reason.
g.script:loadFile("mod_assets/dmcsb_pack/scripts/objects/objectScripts/breakable_door.lua")
end,
onInitialOpen = function(self)
self.go.door:setDoorState("open")
self.go.projectilecollider:disable()
local owtrig = findEntity("owtrig"..self.go.id)
if owtrig then
owtrig.projectilecollider:disable()
else
local g = self.go
local dx,dy = getForward(g.facing)
local nx = g.x+dx
local ny = g.y+dy
if (nx > -1 and nx < 32 and ny > -1 and ny < 32) then
-- if you replace "owtrig" with something else, update dm_door_walltrigger
-- and breakable_door.lua (sorry about this miserable hack)
spawn("dm_door_walltrigger",g.level,nx,ny,(g.facing+2)%4,g.elevation,"owtrig"..g.id)
.projectilecollider:disable()
end
end
end,
onInitialClose = function(self)
self.go.door:setDoorState("closed")
self.go.projectilecollider:enable()
local owtrig = findEntity("owtrig"..self.go.id)
if owtrig then
owtrig.projectilecollider:enable()
else
local g = self.go
local dx,dy = getForward(g.facing)
local nx = g.x+dx
local ny = g.y+dy
if (nx > -1 and nx < 32 and ny > -1 and ny < 32) then
-- if you replace "owtrig" with something else, update dm_door_walltrigger
-- and breakable_door.lua (sorry about this miserable hack)
spawn("dm_door_walltrigger",g.level,nx,ny,(g.facing+2)%4,g.elevation,"owtrig"..g.id)
end
end
end,
onOpen = function(self)
self.go.door:open()
self.go.projectilecollider:disable()
local owtrig = findEntity("owtrig"..self.go.id)
if owtrig then
owtrig.projectilecollider:disable()
else
Console.warn("no outside wall trigger found for "..self.go.id)
end
end,
onClose = function(self)
self.go.door:close()
self.go.projectilecollider:enable()
local owtrig = findEntity("owtrig"..self.go.id)
if owtrig then
owtrig.projectilecollider:enable()
else
Console.warn("no outside wall trigger found for "..self.go.id)
end
end,
onToggle = function(self)
if self.go.door:isClosed() then
self:open()
else
self:close()
end
end,
onStart = function(self)
self.go.door:setDoorState("open")
self.go.projectilecollider:disable()
local owtrig = findEntity("owtrig"..self.go.id)
if owtrig then
owtrig.projectilecollider:disable()
else
Console.warn("no outside wall trigger found for "..self.go.id)
end
end,
onStop = function(self)
self.go.door:setDoorState("closed")
self.go.projectilecollider:enable()
local owtrig = findEntity("owtrig"..self.go.id)
if owtrig then
owtrig.projectilecollider:enable()
else
Console.warn("no outside wall trigger found for "..self.go.id)
end
end,
},
{
class = "WallTrigger",
entityType = "any",
onActivate = function(self,entity)
self.go.script.rangedHit(self,entity)
end,
},
{
class = "ProjectileCollider",
size = vec(2.5, 3, 0.1),
offset = vec(0,1.5,-0.1),
},
},
editorIcon = 128,
tags = {"dm","dm_user","door"},
}
breakable_door.lua:
Code: Select all
-- Fire and electricity explosions will destroy a wooden door in one hit.
WOOD_DESTROYERS = {
fireball_small=true,
fireball_medium=true,
fireball_large=true,
lightning_bolt=true,
lightning_bolt_greater=true,
fire_bomb=true,
shock_bomb=true
}
-- Currently immunities are dummied out, as there is no good way to get the
-- damage type for spell hits.
--[[
IMMUNITIES = {
wood={dispel=true},
stone={dispel=true,fire=true,cold=true,poison=true,shock=true},
metal={dispel=true,fire=true,cold=true,poison=true,shock=true},
}]]
hitSounds = {
wood="dm_hitwood",
stone="impact_blunt",
metal="dm_hitmetal",
}
dieSounds = {
wood="dm_woodbreak",
stone="dm_wallcrumble",
metal="dm_metalbreak",
cloth="wall_tapestry_tear",
}
hitEffects = {
wood="dm_wood_hit",
stone="dm_stone_hit",
metal="dm_metal_hit",
}
dieEffects = {
wood="dm_wood_shatter",
stone="dm_stone_shatter",
metal="dm_metal_shatter",
}
shakeIntensity = {
wood=0.2,
stone=0.3,
metal=0.5,
}
material = "wood"
health = 30
function meleeHit(door, champion, weapon, attack, slot)
-- if not IMMUNITIES[material][attack:getDamageType()] then
-- get a rough estimate of the attack's damage (melee damage formula
-- doesn't correspond to a single rollDamage() call)
-- we can't access critical chance so we just pretend criticals don't exist...
local power = attack:getAttackPower()
if weapon and weapon:hasTrait("heavy_weapon") then
power = power+champion:getSkillLevel("heavy_weapons")*power*0.2
elseif weapon and weapon:hasTrait("light_weapon") then
power = power+champion:getSkillLevel("light_weapons")*power*0.2
end
local stat = attack:getBaseDamageStat()
if stat then power = power+math.max(champion:getCurrentStat(stat)-10,0)*0.75 end
if champion:hasTrait("aggressive") then power = power+4 end
damage(rollDamage(power))
-- end
end
function rangedHit(wallTrigger,entity)
if (material == "wood" or material == "cloth") and entity and WOOD_DESTROYERS[entity.name] then
die(true)
else
if entity and entity.projectile then -- A spell or something.
-- if not IMMUNITIES[material][entity.projectile:getDamageType()] then
damage(rollDamage(entity.projectile:getAttackPower()))
-- end
else -- An item or firearm or something. No good way to figure out attack power, compromise.
damage(rollDamage(10))
end
end
end
-- If incinerated is true, the door was just hit by a fire or electric attack,
-- and if wooden or cloth will have a different particle effect to match.
function die(incinerated)
local g = self.go
g:playSound(dieSounds[material])
local owtrig = findEntity("owtrig"..g.id)
-- curtains get a different model if there's no floor under them
if material == "cloth" and self.go.elevation ~= self.go.map:getElevation(self.go.x,self.go.y) then
g:spawn(g.name.."_brkn_elevated")
else
g:spawn(g.name.."_brkn")
end
local wpos = g:getWorldPosition()
if g.level == party.level and shakeIntensity[material] then
local ppos = party:getWorldPosition()
-- Calculate distance from the party and shake camera if they
-- are close by
local partyDistance = math.sqrt((wpos[1]-ppos[1])^2+(wpos[2]-ppos[2])^2+(wpos[3]-ppos[3])^2)
if partyDistance < 12 then
party.party:shakeCamera(shakeIntensity[material]*(12-partyDistance)/12,shakeIntensity[material])
end
end
if incinerated then
local part = g:spawn("particle_system")
if material == "wood" then
part.particle:setParticleSystem("dm_wood_burn")
part.particle:setEmitterMesh("mod_assets/dmcsb_pack/models/env/dm_fakedoor_wood_broken.fbx")
elseif material == "cloth" then
part.particle:setParticleSystem("dm_cloth_burn")
part.particle:setEmitterMesh(self.go.elevation == self.go.map:getElevation(self.go.x,self.go.y) and "mod_assets/dmcsb_pack/models/env/dm_curtain_torn.fbx" or "mod_assets/dmcsb_pack/models/env/dm_curtain_torn_elevated.fbx")
end
part:setWorldPosition(wpos)
elseif dieEffects[material] then
local part = g:spawn("particle_system")
-- fuzz world position of particle system
if g.facing == 0 or g.facing == 2 then
wpos[1] = wpos[1] + 0.25 - math.random()*0.5
else
wpos[3] = wpos[3] + 0.25 - math.random()*0.5
end
wpos[2] = wpos[2] + 1.75 - math.random()*0.5
part.particle:setParticleSystem(dieEffects[material])
part:setWorldPosition(wpos)
end
local dirts = {
dm_wall_001dirt=true,
dm_wall_bigrocks=true,
dm_wall_grass=true,
dm_wall_mushroom=true,
dm_wall_plant=true,
dm_wall_rocks=true,
dm_wall_wshroom=true,
}
for e in g.map:entitiesAt(g.x,g.y) do
if e.script and e.script.onDoorDestroyed then
e.script.onDoorDestroyed(g)
elseif dirts[e.name] and g.map:getElevation(g.x,g.y) == g.elevation then
e:destroyDelayed()
end
end
if owtrig then
for e in owtrig.map:entitiesAt(owtrig.x,owtrig.y) do
if e.script and e.script.onDoorDestroyed then
e.script.onDoorDestroyed(g)
elseif dirts[e.name] and g.map:getElevation(g.x,g.y) == g.elevation then
e:destroyDelayed()
end
end
owtrig:destroyDelayed()
end
local fd1 = findEntity(self.go.id.."fd1")
if fd1 then fd1:destroy() end
local fd2 = findEntity(self.go.id.."fd2")
if fd2 then fd2:destroy() end
g:destroyDelayed()
end
function damage(amount)
if health > 0 then
health = health-(amount or 1)
if health <= 0 then
die()
else
local g = self.go
if hitEffects[material] then
local part = g:spawn("particle_system")
-- fuzz world position of particle system
local wpos = g:getWorldPosition()
if g.facing == 0 or g.facing == 2 then
wpos[1] = wpos[1] + 0.25 - math.random()*0.5
else
wpos[3] = wpos[3] + 0.25 - math.random()*0.5
end
wpos[2] = wpos[2] + 1.75 - math.random()*0.5
part:setWorldPosition(wpos)
part.particle:setParticleSystem(hitEffects[material])
end
if hitSounds[material] then
g:playSound(hitSounds[material])
end
end
end
end
function setMaterial(mat)
if dieSounds[mat] then
material = mat
else
Console.warn("invalid breakable door material: "..mat)
end
end
function setHealth(newHealth)
health = newHealth
end
If you just want a breakable wall that won't be opened like a door, you can simplify to this:
Code: Select all
defineObject{
name = "dm_fakedoor_wall",
baseObject = "base_secret_door",
components = {
{
class = "Model",
model = "mod_assets/dmcsb_pack/models/env/dm_wall_breakable.fbx",
},
{
class = "Door",
killPillars = false,
secretDoor = true,
--hitSound = "impact_blunt",
onAttackedByChampion = function(self, champion, weapon, attack, slot)
self.go.script.meleeHit(self,champion,weapon,attack,slot)
end,
},
{
class = "Script",
onInit = function(self)
self.setMaterial("stone")
self.setHealth(50)
end,
},
{
class = "WallTrigger",
entityType = "any",
onActivate = function(self,entity)
self.go.script.rangedHit(self,entity)
end,
},
{
class = "ProjectileCollider",
size = vec(2.5, 3, 0.1),
offset = vec(0,1.5,-0.1),
onInit = function(self)
local g = self.go
if not findEntity("owtrig"..self.go.id) then
-- Make a wall trigger on the other side.
local dx,dy = getForward(g.facing)
local nx = g.x+dx
local ny = g.y+dy
if (nx > -1 and nx < 32 and ny > -1 and ny < 32) then
-- if you replace "owtrig" with something else, update dm_door_walltrigger
-- and breakable_door.lua (sorry about this miserable hack)
spawn("dm_door_walltrigger",g.level,nx,ny,(g.facing+2)%4,g.elevation,"owtrig"..g.id)
end
end
-- This doesn't work if I do it in the
-- ScriptComponent.
g.script:loadFile("mod_assets/dmcsb_pack/scripts/objects/objectScripts/breakable_door.lua")
end,
},
},
}