Hey guys. I'm brand new to this. Never made my own dungeon. Never scripted. So I have a question that goes beyond my current knowledge and understanding of such things.
I want to make a "monster" with the existing 3d model of a "wall", but I don't want it to be a monster that moves and attacks. I still want it to be a wall, stationary and uninteresting, but with HP. I want to strategically place walls throughout the dungeon that can be destroyed to reveal hidden paths and secrets. Optionally if it can be made to take damage from only one type of weapon that would also be great. So I figured with some scripting this would be possible. Any thoughts?
Can I make a "Wall" into a "Monster"?
Re: Can I make a "Wall" into a "Monster"?
Using a monster for this would not help in any way.
Grimrock 1 breakable walls do not exist. However, there are breakable obstacles that look like walls. This is okay if your wall will only ever be broken from one side. To do this, just...make a regular breakable obstacle like the barrels, but give it an offset wall model. Check out the Flooded Dungeon, it does this.
In Grimrock 2, you can make fake "breakable" walls that are breakable from both sides, but it's very hacky:
Grimrock 1 breakable walls do not exist. However, there are breakable obstacles that look like walls. This is okay if your wall will only ever be broken from one side. To do this, just...make a regular breakable obstacle like the barrels, but give it an offset wall model. Check out the Flooded Dungeon, it does this.
In Grimrock 2, you can make fake "breakable" walls that are breakable from both sides, but it's very hacky:
SpoilerShow
Here is the code I used for breakable doors in the Dungeon Master Resource:
breakable_door.lua:
If you just want a breakable wall that won't be opened like a door, you can simplify to this:
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"},
}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
endCode: 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,
},
},
}Grimrock 1 dungeon
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
Re: Can I make a "Wall" into a "Monster"?
Awesome. This is plenty for me to work and learn with, and the mods you directed me towards will certainly help. Thanks so much for the prompt response. I've got one more question regarding exits if you or others don't mind. Are exits only usable at the edges of the maps? I want to use the exits for the transition, but I don't want to have to draw a tunnel several tiles out of the way just to use them.
Re: Can I make a "Wall" into a "Monster"?
...Did you mean to post this in the Grimrock 2 subforum? The only exits in Grimrock 1 are stairs, which can be used anywhere and have nothing to do with map edges.
Grimrock 2 adds ExitComponent, and you should only use them at the edges of the 32x32 map, yes. You could use them elsewhere, but you'll need to add teleporters (to fix the party's position when entering the level with the badly placed exit) and the transitions will look wrong on the automap. So you really shouldn't.
Grimrock 2 adds ExitComponent, and you should only use them at the edges of the 32x32 map, yes. You could use them elsewhere, but you'll need to add teleporters (to fix the party's position when entering the level with the badly placed exit) and the transitions will look wrong on the automap. So you really shouldn't.
Grimrock 1 dungeon
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
Grimrock 2 resources
I no longer answer scripting questions in private messages. Please ask in a forum topic or this Discord server.
Re: Can I make a "Wall" into a "Monster"?
Ah my mistake. Yes. This is regarding Grimrock 2. I'll switch over to the correct subforum as a I have another question regarding the LoG2 editor. You've been a great help. Thanks!
Oh, and I figured out how to use the exits in conjunction with the invisible teleporters. The exits spawn you two tiles in from the edge of the map. With the teleporter placed at said location you can use exits for transition wherever you want. It is exactly as you said though. The only issue, if it's an issue, is the automap will have little breaks between zones. Visually though it's seamless.
Oh, and I figured out how to use the exits in conjunction with the invisible teleporters. The exits spawn you two tiles in from the edge of the map. With the teleporter placed at said location you can use exits for transition wherever you want. It is exactly as you said though. The only issue, if it's an issue, is the automap will have little breaks between zones. Visually though it's seamless.