Function to copy or move entire areas of a level

Ask for help about creating mods and scripts for Grimrock 2 or share your tips, scripts, tools and assets with other modders here. Warning: forum contains spoilers!
Post Reply
MrChoke
Posts: 324
Joined: Sat Oct 25, 2014 7:20 pm

Function to copy or move entire areas of a level

Post by MrChoke »

I figured this code is worth sharing. I created the ability to specify an area of a level (X, Y, width, height) and then be able to copy or move all entities in that area to another XY location, another elevation, even another level. And you can rotate the area as well.

Note however, there are limitations to what the game will allow. As always, when you define walls and elevations for a level in the editor those are fixed in-game. So for example, you want to move a building from one place to another. You cannot build the building with walls as defined in the editor. You have to build them with invisible walls and platforms. Because if you build them withls and move the building, the walls will remain, they will be stripped to showing nothing but they are still there. Another problem is where you are moving/copying to. Unless you specify walls and elevations beforehand in the target location, the building will move over but you can walk right thru it because its got no real walls!

Lastly, when moving structures, most of the time you have to define the target location with "void" tiles. If you do not, the floor textures may conflict with the tile textures and it looks messed up. Also, if you move an area, what is left behind may be emptiness. I gave the ability to "fill" this void by specifying the textures in the input object.

To use this, read the comments in the InputObject. This is function: createAreaMoveInputObj().

UPDATED CODE on 12-26-2014

Here is the code plus helper objects

Code: Select all

function createAreaSqrObj(x, y, width, height, levelNum)
    local areaSqrObj = { 
        ["x"] = x, 
        ["y"] = y,
        ["width"] = width,
        ["height"] = height,
        ["levelNum"] = levelNum,
        isXYInArea = 
            function(self, x, y)
                if x >= self.x and x < (self.x + self.width) and y >= self.y and y < (self.y + self.height) then
                    return true
                else
                    return false
                end    
            end,
        isAreaSqrIntersect =
            function(self, areaSqr)
                if self.levelNum == areaSqr.levelNum then
                    local xIntersect = false
                    local yIntersect = false
                
                    if self.x >= areaSqr.x and self.x <= (areaSqr.x + areaSqr.width - 1) or
                        (self.x + self.width - 1) >= areaSqr.x and (self.x + self.width - 1) <= (areaSqr.x + areaSqr.width - 1) then
                            xIntersect = true
                    end
                    if self.y >= areaSqr.y and self.y <= (areaSqr.y + areaSqr.height - 1) or
                        (self.y + self.height - 1) >= areaSqr.y and (self.y + self.height - 1) <= (areaSqr.y + areaSqr.height - 1) then
                            yIntersect = true
                    end      
                    
                    if xIntersect and yIntersect then
                        return true
                    else
                        return false
                    end                    
                else
                    return false
                end
            end,
        apply =
            function(self, applyFunc)
                for x = self.x, self.x + (self.width - 1) do
                    for y = self.y, self.y + (self.height - 1) do
                        applyFunc(x, y)
                    end
                end
            end
    }
  
    return areaSqrObj  
end

--[[
**************************************************************************
    Area Move Process
    Input:  AreaMoveInputObj
    Output: Copy or movement of all entities in an area
**************************************************************************
]]--

-- 1st index is rotateDegree,  2nd index is facing direction.  Value is new facing direction
rotateFacingTbl = {
    [0] = {0, 1, 2, 3},     -- no change
    [-90] = {3, 0, 1, 2},   -- counter-clockwise
    [180] = {2, 3, 0, 1},   -- flip
    [90] = {1, 2, 3, 0},    -- clockwise
}

rotateStartTbl = {  --1st index = deltaX, 2nd = deltaY
    [0] = {0, 0},
    [-90] = {0, 1},
    [180] = {1, 1},
    [90] = {1, 0},
}

rotateXIncTbl = {  --1st index = deltaX, 2nd = deltaY
    [0] = {1, 0},
    [-90] = {0, -1},
    [180] = {-1, 0},
    [90] = {0, 1},
}

rotateYIncTbl = {  --1st index = deltaX, 2nd = deltaY
    [0] = {0, 1},
    [-90] = {1, 0},
    [180] = {0, -1},
    [90] = {-1, 0},
}

rototaePillarTbl = {  --1st index = deltaX, 2nd = deltaY
    [0] = {0, 0},
    [-90] = {0, 1},
    [180] = {1, 1},
    [90] = {1, 0},
}

createAreaMoveInputObj = 
    function(areaSqr)
        local inputObj = {
            ["areaSqr"] = areaSqr,    -- x,y, width, height and level of source area is defined in this object
            ["deltaX"] = 0,     -- Amount to move in the X axis (east west) relative to the source coordinates
            ["deltaY"] = 0,     -- Amount to move in the Y-axis (north south) relative to the source coordinates
            ["deltaZ"] = 0,     -- Amount to elevate (up down) relative to the source elevation
            ["absX"] = nil,     -- Absolute X coordinate (east west) - Both absX and Y must be specified or delta is assumed
            ["absY"] = nil,     -- Absolute Y coordinate (north south).  Both absX and Y must be specified or delta is assumed
            ["newLevelNum"] = nil,  -- Level num of where to spawn new objects.  Leave nil to stay on same level as source
            ["rotateDegrees"] = 0,  -- Rotation degrees.  Valid values are: 0: no rotation,  -90: counter-clockwise, 90: clockwise, 180: Flip  
            ["copyFlag"] = "copy",   -- "copy" to copy an area, leaving the source untouched. 
                                     -- "move" for using setPosition and same IDs as source (some default particles will fail to move using this mode) 
                                     -- "moveDS" - moving source to target but creating new IDs.  This uses spawn and destroy (no game bugs that I am aware of) 
            ["moveFillObjects"] = {"forest_ground_01"},    -- If moving, each voided square will get these objects spawned to fill the void     
            ["moveFillElev"] = 0    -- The elevation at which to fill the void if moving.       
        }    
    
        return inputObj
    end

function areaMoveProcess(inputObj)
    local newLevelNum
    local targetMap
    local newX, newY
    local success = true
    local bUseAbs = false
    
    -- Verify all the inputs first
    if inputObj.absX and inputObj.absY then
        bUseAbs = true
    end    
    
    if inputObj.areaSqr.x < 0 or inputObj.areaSqr.x > 31 or inputObj.areaSqr.y < 0 and inputObj.areaSqr.y > 31 then
        print("Area Move Fail!!  Input XY out of range:", inputObj.areaSqr.x, inputObj.areaSqr.y)
        return false
    end
    
    local x2, y2 = (inputObj.areaSqr.x + inputObj.areaSqr.width - 1), (inputObj.areaSqr.y + inputObj.areaSqr.height - 1) 
    if x2 < 0 or x2 > 31 or y2 < 0 and y2 > 31 then
        print("Area Move Fail!!  Input width/height out of range.  X2,Y2:", x2, y2)
        return false
    end    
    
    if not bUseAbs then    
        newX, newY = (inputObj.areaSqr.x + inputObj.deltaX), (inputObj.areaSqr.y + inputObj.deltaY)
    else
       newX, newY = inputObj.absX, inputObj.absY
    end
    if newX < 0 or newX > 31 or newY < 0 or newY > 31 then
        print("Area Move Fail!!  Target XY out of range:", newX, newY)
        return false
    end    
    
    local newX2, newY2 = (newX + inputObj.areaSqr.width - 1), (newY + inputObj.areaSqr.height - 1) 
    if newX2 < 0 or newX2 > 31 or newY2 < 0 and newY2 > 31 then
        print("Area Move Fail!!  Target width/height out of range.  X2,Y2:", x2, y2)
        return false
    end
    
    local srcMap = Dungeon.getMap(inputObj.areaSqr.levelNum)    
    if not srcMap then
        print("Area Move Fail!! Source map not found for level:", inputObj.areaSqr.levelNum)
        return false
    end    
    
    if inputObj.newLevelNum then        
        newLevelNum = inputObj.newLevelNum
    else
        --No level change
        newLevelNum = inputObj.areaSqr.levelNum
    end
    targetMap = Dungeon.getMap(newLevelNum)
    if not targetMap then
        print("Area Move Fail!! Target map not found for level:", inputObj.areaSqr.levelNum)
        return false
    end
    
    if not rotateFacingTbl[inputObj.rotateDegrees] then
        print("Area Move Fail!!  Invalid rotateDegrees specified.  Use:  0, -90, 90 or 180. value specified was:", inputObj.rotateDegrees)
        return false
    end

    local newAreaSqr = lib_scr.script.createAreaSqrObj(newX, newY, inputObj.areaSqr.width, inputObj.areaSqr.height, newLevelNum)
    local bIntersect = inputObj.areaSqr:isAreaSqrIntersect(newAreaSqr)
    if bIntersect then
        print("Area Move Fail!!  Input area and target area intersect.")
        return false
    end
    
    -- OK, do it
    local entTbl
    local newElev
    local newFacing  
    local startX, startY    
        
    if not bUseAbs then        
        startX = (inputObj.areaSqr.x + inputObj.deltaX) + (rotateStartTbl[inputObj.rotateDegrees][1] * inputObj.areaSqr.width) - rotateStartTbl[inputObj.rotateDegrees][1]
        startY = (inputObj.areaSqr.y + inputObj.deltaY) + (rotateStartTbl[inputObj.rotateDegrees][2] * inputObj.areaSqr.height) - rotateStartTbl[inputObj.rotateDegrees][2]
    else
        startX = inputObj.absX + (rotateStartTbl[inputObj.rotateDegrees][1] * inputObj.areaSqr.width) - rotateStartTbl[inputObj.rotateDegrees][1]
        startY = inputObj.absY + (rotateStartTbl[inputObj.rotateDegrees][2] * inputObj.areaSqr.height) - rotateStartTbl[inputObj.rotateDegrees][2]
    end
    
    newX = startX
    newY = startY
       
    local count = 0    
    for y = inputObj.areaSqr.y, (inputObj.areaSqr.y + inputObj.areaSqr.height - 1) do
        for x = inputObj.areaSqr.x, (inputObj.areaSqr.x + inputObj.areaSqr.width - 1) do
            entTbl = srcMap:entitiesAt(x, y)
            for ent in entTbl do
                newElev = ent.elevation + inputObj.deltaZ
                    
                if newElev < -7 or newElev > 7 then
                    print("Area Move Fail during processing!!  Elevation is out of range.  Source Elev, Source Elevation, XY:", ent.elevation, x, y)
                    return false
                end
                
                newFacing = rotateFacingTbl[inputObj.rotateDegrees][ent.facing+1]
                
                local name = ent.name
                local applyX
                local applyY
                if string.find(ent.name, "pillar") then
                    if not string.find(ent.name, "rock_pillar") then         
                        applyX = newX + rototaePillarTbl[inputObj.rotateDegrees][1]
                        applyY = newY + rototaePillarTbl[inputObj.rotateDegrees][2]
                    else
                        applyX = newX
                        applyY = newY
                    end
                else
                    applyX = newX
                    applyY = newY
                end
                                
                --print("spawn: ",name, newLevelNum, newX, newY, newFacing, newElev)
                if inputObj.copyFlag == "copy" then                        
                    spawn(ent.name, newLevelNum, applyX, applyY, newFacing, newElev)  
                elseif inputObj.copyFlag == "move" then
                    ent:setPosition(applyX, applyY, newFacing, newElev, newLevelNum)
                elseif inputObj.copyFlag == "moveDS" then
                    ent:destroy()
                    spawn(name, newLevelNum, applyX, applyY, newFacing, newElev)  
                else
                    print("Invalid copyFlag value")
                    return false
                end
                
                --[[
                if count > 5 then
                    print("Force abort")
                    success = false
                    break
                end 
                ]]--               
                
                count = count + 1                                                
            end
            
            if not inputObj.copyFlag then
                for i,name in ipairs(inputObj.moveFillObjects) do
                    spawn(name, inputObj.areaSqr.levelNum, x, y, 0, inputObj.moveFillElev)  
                end
            end  
            
            if not success then
                break
            end
                        
            newX = newX + rotateXIncTbl[inputObj.rotateDegrees][1]
            newY = newY + rotateXIncTbl[inputObj.rotateDegrees][2]
        end
        
        if not success then
            break
        end
        
        if rotateYIncTbl[inputObj.rotateDegrees][1] == 0 then
            newX = startX
        else
            newX = newX + rotateYIncTbl[inputObj.rotateDegrees][1] 
        end
        if rotateYIncTbl[inputObj.rotateDegrees][2] == 0 then
            newY = startY
        else
            newY = newY + rotateYIncTbl[inputObj.rotateDegrees][2] 
        end
    end
    
    -- DONE
    print("Area Move completed.  Number of destroy/spawns done: "..count..", success="..tostring(success))
    return success
end
Below is an example of how to call it

Code: Select all

local areaSqr = lib_scr.script.createAreaSqrObj(0, 0, 7, 7, self.go.level)
local inputObj = commonProc_scr.script.createAreaMoveInputObj(areaSqr)
inputObj.absX = 7
inputObj.absY = 21
inputObj.deltaZ = 1
inputObj.newLevelNum = 6
inputObj.rotateDegrees = 90
inputObj.copyFlag = "moveDS"

local success = commonProc_scr.script.areaMoveProcess(inputObj)
print("Level "..self.go.level.." Area Move.  success=", success)             
User avatar
Lark
Posts: 178
Joined: Wed Sep 19, 2012 4:23 pm
Location: Springfield, MO USA

Re: Function to copy or move entire areas of a level

Post by Lark »

MrChoke: This is great! Thank you. It looks like this could solve some of my development issues. I can't wait to give it a try.

Sincerely, -Lark
Post Reply