Page 1 of 1

Can I make a "Wall" into a "Monster"?

Posted: Tue Jan 26, 2016 3:46 am
by Hyperbog
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?

Re: Can I make a "Wall" into a "Monster"?

Posted: Tue Jan 26, 2016 8:51 am
by minmay
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:
SpoilerShow
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,
		},
	},
}

Re: Can I make a "Wall" into a "Monster"?

Posted: Wed Jan 27, 2016 7:48 am
by Hyperbog
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"?

Posted: Wed Jan 27, 2016 10:01 am
by minmay
...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.

Re: Can I make a "Wall" into a "Monster"?

Posted: Thu Jan 28, 2016 5:21 pm
by Hyperbog
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.