# Stone Code

local Chat = class.Chat.new();
local Player = class.Player.new();
local Backpack = class.Backpack.new();
local Block = class.Block.new();
local World = class.World.new();
local WorldContainer = class.WorldContainer.new()
local Trigger = class.Trigger.new()
local TItem = Trigger.Item
--TESTING--
local isDebug = false --Do not commit true --
local function DEBUG(text)
    if isDebug then
        Chat:sendChat(text)
    end
end
local function sendSystemMsg(text)
    Chat:sendSystemMsg(text.." ")
end
--END--

--Const def
local SEPARATE_DISTANCE = 2
local BLOCK_AIR = 0
local BLOCK_ACTIVE = 415
local BLOCK_NORMAL = 667
local BLOCK_FLOOR = 503
local BLOCK_ERROR = 681
local BLOCK_SWITCH = 724
local BLOCK_BOX = 801
local BLOCK_ANSWER = { 671, 672 }
local DATA_ANSWER = { 2, 8 }
local ERROR_TIME = 2
local TOOLS_ID_SEPARATE = 4112
local TOOLS_ID_DYE = { 4111, 4113 }
local TOOLS_ID_DIG = 4114
local TOOLS_STRING = {
    { id = 4097 , string = "Dyeing Gun-Yellow:Dye the stone yellow. " },
    { id = 4098 , string = "Dyeing Gun-Brown:Dye the stone brown. " },
    { id = 4099 , string = "Separating Gun:Separate multiple connected stones. " },
    { id = 4101 , string = "Destroying Gun:Break the extra stones. " },
}

local ALLOWED_ITEMS = {
    12963, 12550, 12003, 12050, 12574, 4100,
    TOOLS_ID_SEPARATE, TOOLS_ID_DYE[1], TOOLS_ID_DYE[2], TOOLS_ID_DIG, }
local function POSITION(x, y, z) return { x = x, y = y, z = z } end
local LEVEL_CONFIG = {
    {
        --1
        PUZZLE = { POSITION(-51, 10, 17), POSITION(-51, 10, 17) },
        ANSWER = { POSITION(-39, 8, 12), POSITION(-39, 8, 12) },
        RESET = { POS = POSITION(-48, 10, 17) },
        REWARD = { POS = POSITION(-53, 8, 20), ID = 12963 },
    },
    {
        --2
        PUZZLE = { POSITION(-60, 10, 13), POSITION(-56, 10, 13) },
        ANSWER = { POSITION(-38, 9, 16), POSITION(-34, 9, 16) },
        RESET = { POS = POSITION(-63, 10, 15) },
        REWARD = { POS = POSITION(-58, 8, 11), ID = 12550 },
    },
    {
        --3
        PUZZLE = { POSITION(-59, 10, 27), POSITION(-57, 13, 27) },
        ANSWER = { POSITION(-39, 9, 24), POSITION(-37, 12, 24) },
        RESET = { POS = POSITION(-62, 10, 31) },
        REWARD = { POS = POSITION(-60, 9, 31), ID = 12003 },
    },
    {
        --4
        PUZZLE = { POSITION(-51, 10, 25), POSITION(-51, 13, 27) },
        ANSWER = { POSITION(-33, 8, 22), POSITION(-33, 11, 24) },
        RESET = { POS = POSITION(-48, 10, 27) },
        REWARD = { POS = POSITION(-48, 9, 24), ID = 12050 },
    },
    {
        --5
        PUZZLE = { POSITION(-78, 12, 23), POSITION(-76, 16, 25) },
        ANSWER = { POSITION(-78, 2, 23), POSITION(-76, 6, 25) },
        RESET = { POS = POSITION(-89, 10, 24) },
        REWARD = { POS = POSITION(-89, 9, 27), ID = 12574},
    },
    {
        --6
        PUZZLE = { POSITION(-112, 24, -48), POSITION(-107, 28, -45) },
        ANSWER = { POSITION(-111, 40, -46), POSITION(-106, 44, -43) },
        RESET = { POS = POSITION(-129, 12, -22) },
        MONSTER = {
            POS = { POSITION( -135, 16, -20 ), POSITION(-135, 16, -71),
                    POSITION(-84, 16, -71), POSITION(-84, 16, -20) },
            ID = 2, NUM = 1 },
        TRANSFER = { POS = POSITION( -129, 5, -26) },
        REWARD = { POS = POSITION(-110, 19, -45), ID = 4100},
    },
    {
        --7 Ostrich
        PUZZLE = { POSITION(7, 14, -49), POSITION(11, 18, -47) },
        ANSWER = { POSITION(10, 33, -50), POSITION(14, 37, -48) },
        RESET = { POS = POSITION(-8, 6, -45) },
        MONSTER = {
            POS = { POSITION( 4, 3, -39 ), POSITION(16, 3, -39),
                    POSITION(16, 3, -57), POSITION(4, 3, -57) },
            ID = 3, NUM = 1 },
        TRANSFER = { POS = POSITION( -11, 1, -48) },
        REWARD = { POS = POSITION(9, 9, -48), ID = 4100},
    },
    {
        --8
        PUZZLE = { POSITION(-51, 15, -45), POSITION(-44, 22, -43) },
        ANSWER = { POSITION(-53, 31, -45), POSITION(-46, 38, -43) },
        RESET = { POS = POSITION(-71, 15, -43) },
        MONSTER = {
            POS = { POSITION( -34, 8, -58), POSITION(-61, 8, -58),
                    POSITION(-61, 8, -31), POSITION(-34, 8, -31) },
            ID = 4, NUM = 1 },
        TRANSFER = { POS = POSITION( -71, 11, -45) },
        REWARD = { POS = POSITION(-47, 9, -44), ID = 4100},
    },
}

local errorPoints = {}
local areas = {}
local ID_prompts = {
    {
        {
            3989, 3988, 3987, 3986, 3985, 3984, 3983, 3982, 3981, 3979, 3978, 3977,
            3976, 3975, 3974, 3973, 3972, 3969, 3968, 3967, 3966, 3965, 3964, 3963,
            zero = 3990
        },
        {
            3961, 3960, 3959, 3958, 3957, 3956, 3955, 3954, 3953, 3951, 3950, 3949,
            3948, 3947, 3946, 3945, 3944, 3941, 3940, 3939, 3938, 3937, 3936, 3935,
            zero = 3962
        }
    },
    {
        {
            3894, 3893, 3892, 3891, 3890, 3889, 3888, 3887, 3886, 3878, 3880, 3879,
            3881, 3882, 3883, 3884, 3885, 3877, 3876, 3875, 3874, 3873, 3872, 3871,
            zero = 3870
        },
        {
            3868, 3867, 3866, 3865, 3864, 3863, 3862, 3861, 3860, 3852, 3853, 3854,
            3855, 3856, 3857, 3858, 3859, 3851, 3850, 3849, 3848, 3847, 3846, 3845,
            zero = 3869
        }
    },
    {
        {
            3999, 3998, 3997, 3996, 3995, 3994, 3993, 3992, 3991, 3934, 3933, 3932,
            3931, 3930, 3929, 3928, 3927, 3926, 3925, 3924, 3923, 3922, 3921, 3920,
            zero = 4000
        },
        {
            3918, 3917, 3916, 3915, 3914, 3913, 3912, 3911, 3910, 3909, 3908, 3907,
            3906, 3905, 3904, 3903, 3902, 3901, 3900, 3899, 3898, 3897, 3896, 3895,
            zero = 3919
        }
    },
    max = 4000, min = 3845
}
--Const end

local function isPrompt(id)
    for i = 1, #ID_prompts do
        local side = ID_prompts[i]
        for j = 1, #side do
            local color = side[j]
            if color.zero == id then
                return i
            end
            for k = 1, #color do
                if color[k] == id then
                    return i
                end
            end
        end
    end
    return 0
end

local function isInclude(value, _table)
    if not _table then
        return
    end
    for k,v in ipairs(_table) do
        if v == value then
            return true
        end
    end
    return false
end

function updateAreaFlats(area)
    local p1 = area.curPos[1]
    local p2 = area.curPos[2]

    area.flats = {}
    table.insert(area.flats, { POSITION(p1.x, p2.y, p2.z), POSITION(p1.x, p1.y, p1.z) })
    table.insert(area.flats, { POSITION(p2.x, p2.y, p2.z), POSITION(p2.x, p1.y, p1.z) })
    table.insert(area.flats, { POSITION(p2.x, p2.y, p1.z), POSITION(p1.x, p1.y, p1.z) })
    table.insert(area.flats, { POSITION(p2.x, p2.y, p2.z), POSITION(p1.x, p1.y, p2.z) })
    table.insert(area.flats, { POSITION(p1.x, p1.y, p1.z), POSITION(p2.x, p1.y, p2.z) })
    table.insert(area.flats, { POSITION(p2.x, p2.y, p2.z), POSITION(p1.x, p2.y, p1.z) })
end

function updateAreaSize(area)
    local p1 = area.curPos[1]
    local p2 = area.curPos[2]

    area.size = {}
    area.size.length = math.abs(p1.x - p2.x) + 1
    area.size.width = math.abs(p1.z - p2.z) + 1
    area.size.height = math.abs(p1.y - p2.y) + 1
end

function getAnswerID(area)
    local answer = area.answer
    area.ids = {}
    for y = answer.curPos[1].y, answer.curPos[1].y+answer.size.height-1 do
        for x = answer.curPos[1].x, answer.curPos[1].x+answer.size.length-1 do
            for z = answer.curPos[1].z, answer.curPos[1].z+answer.size.width-1 do
                local ret, id = Block:getBlockID(x, y, z)
                if id ~= 0 and not isInclude(id, area.ids) then
                    table.insert(area.ids, id);
                end
            end
        end
    end
end

function cheat(area)
    local puzzle = area.puzzle
    for y = puzzle.curPos[1].y, puzzle.curPos[1].y+puzzle.size.height-1 do
        for x = puzzle.curPos[1].x, puzzle.curPos[1].x+puzzle.size.length-1 do
            for z = puzzle.curPos[1].z, puzzle.curPos[1].z+puzzle.size.width-1 do
                local ret, blockID = Block:getBlockID(x, y, z)
                local targetPoint = mapping(area, POSITION(x, y, z))
                local ret, targetID = Block:getBlockID(targetPoint.x, targetPoint.y, targetPoint.z)
                if blockID ~= targetID then
                    Block:setBlockAll(x, y, z, targetID)
                end
            end
        end
    end
    checkCompletion(area)
end

function checkCompletion(area)
    if area.state == 1 then
        return
    end
    local num = 0
    local puzzle = area.puzzle
    for y = puzzle.curPos[1].y, puzzle.curPos[1].y+puzzle.size.height-1 do
        for x = puzzle.curPos[1].x, puzzle.curPos[1].x+puzzle.size.length-1 do
            for z = puzzle.curPos[1].z, puzzle.curPos[1].z+puzzle.size.width-1 do
                local ret, id = Block:getBlockID(x, y, z)
                if id == BLOCK_NORMAL or id == BLOCK_ERROR then
                    return
                end
            end
        end
    end
    local axes = { 'x', 'x', 'z', 'z', 'y', 'y'}
    if hasSeparatePart(area.separation, axes[1]) then
        local history = area.separation.history[axes[1]]

        if history then
            local p1 = puzzle.flats[history.flatIndex][1]
            local p2 = puzzle.flats[history.flatIndex][2]
            local values = { 1, -1, 1, -1, 1, -1}
            if history.num > 1 then
                for i = 1, history.num-2 do
                    p1[axes[history.dir]] = p1[axes[history.dir]] - values[history.dir]
                    p2[axes[history.dir]] = p2[axes[history.dir]] - values[history.dir]
                end
            end
            local size = POSITION(puzzle.size.length+2, puzzle.size.height+2, puzzle.size.width+2)

            move({p1, p2}, history.dir, history.num, size, history.vertex)

            area.separation.times[axes[1]] = 0
            area.separation.history[axes[1]] = nil
        end
    end
    if hasSeparatePart(area.separation, axes[3]) then
        local history = area.separation.history[axes[3]]

        if history then
            local p1 = puzzle.flats[history.flatIndex][1]
            local p2 = puzzle.flats[history.flatIndex][2]
            local values = { 1, -1, 1, -1, 1, -1}
            if history.num > 1 then
                for i = 1, history.num-2 do
                    p1[axes[history.dir]] = p1[axes[history.dir]] - values[history.dir]
                    p2[axes[history.dir]] = p2[axes[history.dir]] - values[history.dir]
                end
            end
            local size = POSITION(puzzle.size.length+2, puzzle.size.height+2, puzzle.size.width+2)

            move({p1, p2}, history.dir, history.num, size, history.vertex)

            area.separation.times[axes[3]] = 0
            area.separation.history[axes[3]] = nil
        end
    end
    updateAreaFlats(puzzle)
    updateAreaSize(puzzle)
    for y = puzzle.curPos[1].y, puzzle.curPos[1].y+puzzle.size.height-1 do
        for x = puzzle.curPos[1].x, puzzle.curPos[1].x+puzzle.size.length-1 do
            for z = puzzle.curPos[1].z, puzzle.curPos[1].z+puzzle.size.width-1 do
                if not verifySingleBlockInAnswerArea(area, POSITION(x, y, z)) then
                    return
                end
            end
        end
    end
    for j = 1, #area.answer.flats do
        cleanDigitalPrompt(area, area.puzzle.flats[j], j)
    end
    for y = puzzle.curPos[1].y, puzzle.curPos[1].y+puzzle.size.height-1 do
        for x = puzzle.curPos[1].x, puzzle.curPos[1].x+puzzle.size.length-1 do
            for z = puzzle.curPos[1].z, puzzle.curPos[1].z+puzzle.size.width-1 do
                local ret, id = Block:getBlockID(x, y, z)
                if isPrompt(id) ~= 0 then
                    Block:setBlockAll(x, y, z, BLOCK_AIR)
                end
            end
        end
    end

    sendSystemMsg("Congratulations! You have completed the mission and a new gift show up in the Storage Box. ")
    area.state = 1

    if LEVEL_CONFIG[area.index].MONSTER then
        generateMonster(area)
    end
    if LEVEL_CONFIG[area.index].TRANSFER then
        local tr = LEVEL_CONFIG[area.index].TRANSFER.POS
        Block:setBlockAll(tr.x, tr.y, tr.z, BLOCK_ACTIVE)
        sendSystemMsg("A new portal is activated. ")
    end

    if LEVEL_CONFIG[area.index].REWARD then
        generateReward(area)
    end
end

function generateMonster(area)
    local monster = LEVEL_CONFIG[area.index].MONSTER
    local pos = monster.POS
    area.monsterids = {}
    for i = 1, #pos do
        local p = pos[i]
        local ret, objids = World:spawnCreature(p.x, p.y, p.z, monster.ID, monster.NUM)
        if objids then
            table.insert(area.monsterids, objids[1])
        end
    end
    sendSystemMsg("Crazy animals appear! Beat them! ")
end

function generateReward(area)
    local reward = LEVEL_CONFIG[area.index].REWARD
    local pos = reward.POS
    local ret, id = Block:getBlockID(pos.x, pos.y, pos.z)
    if id ~= BLOCK_BOX then
        Block:setBlockAll(pos.x, pos.y, pos.z, BLOCK_BOX)
    end
    local ret, itemCnt = WorldContainer:addItemToContainer(pos.x, pos.y, pos.z, reward.ID, 1)
    if ret == ErrorCode.OK then

    end
end

function isInArea(point, area)
    local curP1 = area.curPos[1]
    local curP2 = area.curPos[2]
    if point.x >= math.min(curP1.x, curP2.x) and point.x <= math.max(curP1.x, curP2.x) then
        if point.y >= math.min(curP1.y, curP2.y) and point.y <= math.max(curP1.y, curP2.y) then
            if point.z >= math.min(curP1.z, curP2.z) and point.z <= math.max(curP1.z, curP2.z) then
                return true
            end
        end
    end
    return false
end

function isInFlat(point, flat)
    local curP1 = flat[1]
    local curP2 = flat[2]
    if point.x >= math.min(curP1.x, curP2.x) and point.x <= math.max(curP1.x, curP2.x) then
        if point.y >= math.min(curP1.y, curP2.y) and point.y <= math.max(curP1.y, curP2.y) then
            if point.z >= math.min(curP1.z, curP2.z) and point.z <= math.max(curP1.z, curP2.z) then
                return true
            end
        end
    end
    return false
end

function updateSidesDigitalPrompt(pos)
    local axes = { 'x', 'x', 'z', 'z', 'y', 'y' }
    local values = { -1, 1, -1, 1, 1, -1 }
    local dirs = { 4, 1, 2, 3, 1, 1 }

    for i = 1, 6 do
        local p = POSITION(pos.x, pos.y, pos.z)
        p[axes[i]] = p[axes[i]] + values[i]
        local ret, id = Block:getBlockID(p.x, p.y, p.z)
        local side = isPrompt(id)
        if side ~= 0 then
            local ret, data = Block:getBlockData(p.x, p.y, p.z)
            if data == dirs[i] then
                if side == 1 and i <= 4 or side == 3 and i == 5 or side == 2 and i == 6 then
                    Block:setBlockAll(pos.x, pos.y, pos.z, id, data)
                    Block:destroyBlock(p.x, p.y, p.z)
                    local value = -values[i]
                    local tp = POSITION(pos.x, pos.y, pos.z)
                    while(true) do
                        tp[axes[i]] = tp[axes[i]] + value
                        local ret, nid = Block:getBlockID(tp.x, tp.y, tp.z)
                        local ret, ndata = Block:getBlockData(tp.x, tp.y, tp.z)
                        local side = isPrompt(nid)
                        if side ~= 0 then
                            if data + ndata == 5 then
                                Block:setBlockAll(pos.x, pos.y, pos.z, BLOCK_AIR)
                                Block:setBlockAll(tp.x, tp.y, tp.z, BLOCK_AIR)
                                break
                            else

                            end
                        else
                            break
                        end

                    end

                    break
                end
            end
        else
        end
    end
end

function updateDigitalPrompt(area, flat, dir)
    local size
    local isInit = false
    local prompt = {}
    local count = 0

    if area.prompts and area.prompts[dir] then
        size = area.puzzle.size
        prompt = area.prompts[dir]
        count = 1
    else
        if not area.prompts then
            area.prompts = {}
        end
        size = area.answer.size
        area.prompts[dir] = prompt
        isInit = true
    end

    local p1, p2
    local function CONFIG(axes, signs)
        local c = { axes = axes, signs = signs }
        return c
    end
    local c = {
        CONFIG({'y', 'z', 'x'}, {-1, -1, 1}),
        CONFIG({'y', 'z', 'x'}, {-1, -1, -1}),
        CONFIG({'y', 'x', 'z'}, {-1, -1, 1}),
        CONFIG({'y', 'x', 'z'}, {-1, -1, -1}),
        CONFIG({'x', 'z', 'y'}, {-1, -1, 1}),
        CONFIG({'x', 'z', 'y'}, {-1, -1, -1}),
    }
    local limits = POSITION(size.length, size.height, size.width)
    c = c[dir]

    p1 = flat[1]
    p2 = flat[2]

    for i = 1, #c.axes-1 do
        local axis = c.axes[i]
        if p1[axis] > p2[axis] then
            c.signs[i] = -1
        elseif p1[axis] < p2[axis] then
            c.signs[i] = 1
        else
            c.signs[i] = 0
        end
    end

    local blockid = BLOCK_ANSWER[dir % 2 + 1]

    local t1 = POSITION(p1.x, p1.y, p1.z)
    for i = 1, limits[c.axes[1]] do
        local t2 = POSITION(t1.x, t1.y, t1.z)
        for j = 1, limits[c.axes[2]] do
            local t3 = POSITION(t2.x, t2.y, t2.z)

            if isInit then
                local part = 0
                local isContinuous = false
                local num = 0
                local promptID
                for k = 1, limits[c.axes[3]] do
                    local ret, id = Block:getBlockID(t3.x, t3.y, t3.z)
                    if id == blockid then
                        num = num + 1
                        if not isContinuous then
                            part = part + 1
                            isContinuous = true
                        end

                    elseif isContinuous then
                        isContinuous = false
                    end
                    t3[c.axes[3]] = t3[c.axes[3]] + c.signs[3]
                end

                local index1 = 1
                if dir <= 4 then

                else
                    index1 = dir - 3
                end
                if num == 0 then
                    promptID = ID_prompts[index1][dir%2+1].zero
                else
                    local partToNum = { 0, 8, 15, 24}
                    promptID = ID_prompts[index1][dir%2+1][num+partToNum[part]]
                end

                table.insert(prompt, promptID)
            else
                local setP = POSITION(t2.x, t2.y, t2.z)
                local toFace = { 4, 1, 2, 3, 1, 1 }
                local toCoord = { 'x', 'x', 'z', 'z', 'y', 'y' }
                local toSign = { -1, 1, -1, 1, -1, 1 }
                setP[toCoord[dir]] = setP[toCoord[dir]] + toSign[dir]
                Block:setBlockAll(setP.x, setP.y, setP.z, prompt[count], toFace[dir])
                count = count + 1
            end

            t2[c.axes[2]] = t2[c.axes[2]] + c.signs[2]
        end

        t1[c.axes[1]] = t1[c.axes[1]] + c.signs[1]
    end

end

function cleanDigitalPrompt(area, flat, dir)
    local size
    local isInit = false
    local prompt = {}
    local count = 0

    if area.prompts and area.prompts[dir] then
        size = area.puzzle.size
        prompt = area.prompts[dir]
        count = 1
    else
        if not area.prompts then
            area.prompts = {}
        end
        size = area.answer.size
        area.prompts[dir] = prompt
        isInit = true
    end

    local p1, p2
    local function CONFIG(axes, signs)
        local c = { axes = axes, signs = signs }
        return c
    end
    local c = {
        CONFIG({'y', 'z', 'x'}, {-1, -1, 1}),
        CONFIG({'y', 'z', 'x'}, {-1, -1, -1}),
        CONFIG({'y', 'x', 'z'}, {-1, -1, 1}),
        CONFIG({'y', 'x', 'z'}, {-1, -1, -1}),
        CONFIG({'x', 'z', 'y'}, {-1, -1, 1}),
        CONFIG({'x', 'z', 'y'}, {-1, -1, -1}),
    }
    local limits = POSITION(size.length, size.height, size.width)
    c = c[dir]

    p1 = flat[1]
    p2 = flat[2]

    for i = 1, #c.axes-1 do
        local axis = c.axes[i]
        if p1[axis] > p2[axis] then
            c.signs[i] = -1
        elseif p1[axis] < p2[axis] then
            c.signs[i] = 1
        else
            c.signs[i] = 0
        end
    end

    local blockid = BLOCK_ANSWER[dir % 2 + 1]

    local t1 = POSITION(p1.x, p1.y, p1.z)
    for i = 1, limits[c.axes[1]] do
        local t2 = POSITION(t1.x, t1.y, t1.z)
        for j = 1, limits[c.axes[2]] do
            local t3 = POSITION(t2.x, t2.y, t2.z)

            if isInit then
                local part = 0
                local isContinuous = false
                local num = 0
                local promptID
                for k = 1, limits[c.axes[3]] do
                    local ret, id = Block:getBlockID(t3.x, t3.y, t3.z)
                    if id == blockid then
                        num = num + 1
                        if not isContinuous then
                            part = part + 1
                            isContinuous = true
                        end

                    elseif isContinuous then
                        isContinuous = false
                    end
                    t3[c.axes[3]] = t3[c.axes[3]] + c.signs[3]
                end

                local index1 = 1
                if dir <= 4 then

                else
                    index1 = dir - 3
                end
                if num == 0 then
                    promptID = ID_prompts[index1][dir%2+1].zero
                else
                    local partToNum = { 0, 8, 15, 24}
                    promptID = ID_prompts[index1][dir%2+1][num+partToNum[part]]
                end

                table.insert(prompt, promptID)
            else
                local setP = POSITION(t2.x, t2.y, t2.z)
                local toFace = { 4, 1, 2, 3, 1, 1 }
                local toCoord = { 'x', 'x', 'z', 'z', 'y', 'y' }
                local toSign = { -1, 1, -1, 1, -1, 1 }
                setP[toCoord[dir]] = setP[toCoord[dir]] + toSign[dir]
                Block:setBlockAll(setP.x, setP.y, setP.z, BLOCK_AIR, toFace[dir])
                count = count + 1
            end

            t2[c.axes[2]] = t2[c.axes[2]] + c.signs[2]
        end

        t1[c.axes[1]] = t1[c.axes[1]] + c.signs[1]
    end

end

function mapping(area, pos)
    local puzzle = area.puzzle
    local offsetP = POSITION(pos.x, pos.y, pos.z)
    local values = { 1, -1, 1, -1, 1, -1}

    local separation = area.separation
    if hasSeparatePart(separation, 'x') then
        local history = separation.history.x
        local coord = 'z'
        if offsetP[coord] >= math.min(history.vertex[coord], history.vertex[coord]-values[history.dir]*history.num)
                and offsetP[coord] <= math.max(history.vertex[coord], history.vertex[coord]-values[history.dir]*history.num) then
            offsetP[coord] = offsetP[coord] - values[history.dir] * SEPARATE_DISTANCE
        end
    end
    if hasSeparatePart(separation, 'z') then
        local history = separation.history.z
        local coord = 'x'
        if offsetP[coord] >= math.min(history.vertex[coord], history.vertex[coord]-values[history.dir]*history.num)
                and offsetP[coord] <= math.max(history.vertex[coord], history.vertex[coord]-values[history.dir]*history.num) then
            offsetP[coord] = offsetP[coord] - values[history.dir] * SEPARATE_DISTANCE
        end
    end

    local puzzlePoint = puzzle.srcPos[1]
    local answerPoint = area.answer.srcPos[1]
    local vector = {}
    vector.x = offsetP.x - puzzlePoint.x
    vector.y = offsetP.y - puzzlePoint.y
    vector.z = offsetP.z - puzzlePoint.z

    return POSITION(vector.x + answerPoint.x, vector.y + answerPoint.y, vector.z + answerPoint.z)
end

function hasSeparatePart(separation, axis)
    if separation.times[axis] and separation.times[axis] ~= 0 then
        return true
    else
        return false
    end
end

function separate(area, hitP, hitDir)
    --hitDir = hitDir + 1
    local text = ""
    local puzzle = area.puzzle
    local flats = puzzle.flats
    local axes = { 'x', 'x', 'z', 'z', 'y', 'y'}
    local verticalAxes = { 'z', 'z', 'x', 'x', 'y', 'y'}

    local isCenter = false

    if hitP[verticalAxes[hitDir]] == puzzle.midValues[hitDir] then
        isCenter = true
        text = text .. " isCenter"
    end
    if puzzle.size.width == 1 and puzzle.size.height == 1 and puzzle.size.length == 1 then
        return
    elseif puzzle.size.length == 1 and hitDir >= 3 and hitDir <= 4 then
        return
    elseif puzzle.size.width == 1 and hitDir >= 1 and hitDir <= 2 then
        return
    end

    if hasSeparatePart(area.separation, axes[hitDir]) then
        local history = area.separation.history[axes[hitDir]]

        if history then
            local p1 = flats[history.flatIndex][1]
            local p2 = flats[history.flatIndex][2]
            local flat = { POSITION(p1.x, p1.y, p1.z), POSITION(p2.x, p2.y, p2.z) }
            local values = { 1, -1, 1, -1, 1, -1}
            if history.num > 1 then
                for i = 1, history.num-2 do
                    p1[axes[history.dir]] = p1[axes[history.dir]] - values[history.dir]
                    p2[axes[history.dir]] = p2[axes[history.dir]] - values[history.dir]
                end
            end
            local size = POSITION(puzzle.size.length+2, puzzle.size.height+2, puzzle.size.width+2)

            move({p1, p2}, history.dir, history.num, size, history.vertex)

            area.separation.times[axes[hitDir]] = 0
            area.separation.history[axes[hitDir]] = nil
        end
    else
        local verticalAxis = verticalAxes[hitDir]
        local value
        local num
        if isCenter then
            local randomNum = { 2, 1 }
            math.randomseed(tostring(os.time()):reverse():sub(1, 7))
            value = randomNum[math.random(2)]
            num = 0
        else
            if hitP[verticalAxis] > puzzle.midValues[hitDir] then
                value = 2
            else
                value = 1
            end
            num = 1
        end

        if value == 2 then
            num = num + math.max(puzzle.curPos[1][verticalAxis], puzzle.curPos[2][verticalAxis]) - hitP[verticalAxis]
        else
            num = num + hitP[verticalAxis] - math.min(puzzle.curPos[1][verticalAxis], puzzle.curPos[2][verticalAxis])
        end
        num = num + 1

        local moveDir = { { 3, 4 }, { 3, 4 }, { 1, 2 }, { 1, 2 } }

        local flat = flats[moveDir[hitDir][value]]
        local p1 = POSITION(flat[1].x, flat[1].y, flat[1].z)
        local p2 = POSITION(flat[2].x, flat[2].y, flat[2].z)
        local vertex
        for i = 1, #puzzle.curPos do
            if p1.x == puzzle.curPos[i].x and p1.y == puzzle.curPos[i].y and p1.z == puzzle.curPos[i].z
                    or p2.x == puzzle.curPos[i].x and p2.y == puzzle.curPos[i].y and p2.z == puzzle.curPos[i].z then
                vertex = puzzle.curPos[i]
            end
        end

        local toAxes = { 'x', 'x', 'z', 'z', 'y', 'y'}
        local values = { 1, -1, 1, -1, 1, -1}
        local axis = toAxes[moveDir[hitDir][value]]
        local moveValue = values[moveDir[hitDir][value]]
        local size = POSITION(puzzle.size.length+2, puzzle.size.height+2, puzzle.size.width+2)

        p1[axis] = p1[axis] - moveValue
        p2[axis] = p2[axis] - moveValue

        move({p1, p2}, moveDir[hitDir][value], num, size, vertex)

        area.separation.times[axes[hitDir]] = 1

        local history = { num = num, flatIndex = moveDir[hitDir][value], vertex = vertex }
        if value == 1 then
            value = 2
        else
            value = 1
        end
        history.dir = moveDir[hitDir][value]
        area.separation.history[axes[hitDir]] = history
    end

    updateAreaFlats(puzzle)
    updateAreaSize(puzzle)
end

function move(flat, dir, num, size, vertex)
    text = string.format("Move dir=%d, num=%d", dir, num)
    local p1 = POSITION(flat[1].x, flat[1].y, flat[1].z)
    local p2 = POSITION(flat[2].x, flat[2].y, flat[2].z)
    local toAxes = { 'x', 'x', 'z', 'z', 'y', 'y'}
    local values = { 1, -1, 1, -1, 1, -1}
    local axis = toAxes[dir]

    local axes = { 'x', 'y', 'z'}
    local axisIndex
    for i = 1, #axes do
        if axis == axes[i] then
            axisIndex = i
        else
            if p1[axes[i]] > p2[axes[i]] then
                p1[axes[i]] = p1[axes[i]] + 1
                p2[axes[i]] = p2[axes[i]] + -1
            elseif p1[axes[i]] < p2[axes[i]] then
                p1[axes[i]] = p1[axes[i]] + -1
                p2[axes[i]] = p2[axes[i]] + 1
            else
                p1[axes[i]] = p1[axes[i]] + 1
                p2[axes[i]] = p2[axes[i]] + -1
            end
        end
    end
    table.remove(axes, axisIndex)
    values = { values[dir] }
    for i = 1, #axes do
        if p1[axes[i]] > p2[axes[i]] then
            values[i+1] = -1
        else
            values[i+1] = 1
        end
    end

    local limits = size
    local t1 = POSITION(p1.x, p1.y, p1.z)
    for i = 1, num do
        local t2 = POSITION(t1.x, t1.y, t1.z)
        for j = 1, limits[axes[1]] do
            local t3 = POSITION(t2.x, t2.y, t2.z)
            for k = 1, limits[axes[2]] do
                local moveP = POSITION(t3.x, t3.y, t3.z)
                local ret, id = Block:getBlockID(t3.x, t3.y, t3.z)
                local ret, data = Block:getBlockData(t3.x, t3.y, t3.z)
                moveP[axis] = moveP[axis] - values[1] * SEPARATE_DISTANCE
                if id == BLOCK_ERROR then
                    id = BLOCK_NORMAL
                end
                Block:setBlockAll(moveP.x, moveP.y, moveP.z, id, data)
                Block:destroyBlock(t3.x, t3.y, t3.z, false)

                t3[axes[2]] = t3[axes[2]] + values[3]
            end
            t2[axes[1]] = t2[axes[1]] + values[2]
        end
        t1[axis] = t1[axis] + values[1]
    end

    vertex[axis] = vertex[axis] - values[1] * SEPARATE_DISTANCE
end

function dye(area, pos, tool)
    local index = 0
    if tool == TOOLS_ID_DYE[1] then
        index = 1
    else
        index = 2
    end

    Block:setBlockAll(pos.x, pos.y, pos.z, BLOCK_ANSWER[index], DATA_ANSWER[index])

    if verifySingleBlockInAnswerArea(area, pos) then
        checkCompletion(area)
    else
        setErrorBlock(pos)
    end
end

function setErrorBlock(pos)
    Block:setBlockAll(pos.x, pos.y, pos.z, BLOCK_ERROR)
    table.insert(errorPoints, {pos = pos, time = os.time()})
end

function verifySingleBlockInAnswerArea(area, pos)
    local targetPoint = mapping(area, pos)
    local ret, hitID = Block:getBlockID(pos.x, pos.y, pos.z)
    local ret, targetID = Block:getBlockID(targetPoint.x, targetPoint.y, targetPoint.z)
    local text = tostring(hitID)  .. " : " .. tostring(targetID)
    if isPrompt(hitID) ~= 0 then
        hitID = BLOCK_AIR
    end
    if hitID == targetID then
        return true
    else
        --Block:setBlockAll(pos.x, pos.y, pos.z, BLOCK_ERROR)
        --table.insert(errorPoints, {pos = pos, time = os.time()})
        return false
    end
end

function startReset(area)
    local puzzle = area.puzzle
    area.reset.num = 1
    area.reset.pos = POSITION(puzzle.srcPos[1].x, puzzle.srcPos[1].y, puzzle.srcPos[1].z)
    local axes = { 'x', 'x', 'z', 'z', 'y', 'y'}
    if hasSeparatePart(area.separation, axes[1]) then
        local history = area.separation.history[axes[1]]

        if history then
            local p1 = puzzle.flats[history.flatIndex][1]
            local p2 = puzzle.flats[history.flatIndex][2]
            local values = { 1, -1, 1, -1, 1, -1}
            if history.num > 1 then
                for i = 1, history.num-2 do
                    p1[axes[history.dir]] = p1[axes[history.dir]] - values[history.dir]
                    p2[axes[history.dir]] = p2[axes[history.dir]] - values[history.dir]
                end
            end
            local size = POSITION(puzzle.size.length+2, puzzle.size.height+2, puzzle.size.width+2)

            move({p1, p2}, history.dir, history.num, size, history.vertex)

            area.separation.times[axes[1]] = 0
            area.separation.history[axes[1]] = nil
        end
    end
    if hasSeparatePart(area.separation, axes[3]) then
        local history = area.separation.history[axes[3]]

        if history then
            local p1 = puzzle.flats[history.flatIndex][1]
            local p2 = puzzle.flats[history.flatIndex][2]
            local values = { 1, -1, 1, -1, 1, -1}
            if history.num > 1 then
                for i = 1, history.num-2 do
                    p1[axes[history.dir]] = p1[axes[history.dir]] - values[history.dir]
                    p2[axes[history.dir]] = p2[axes[history.dir]] - values[history.dir]
                end
            end
            local size = POSITION(puzzle.size.length+2, puzzle.size.height+2, puzzle.size.width+2)

            move({p1, p2}, history.dir, history.num, size, history.vertex)

            area.separation.times[axes[3]] = 0
            area.separation.history[axes[3]] = nil
        end
    end
    updateAreaFlats(puzzle)
    updateAreaSize(puzzle)
    area.isResetting = true
    area.state = 0
end

function resetting(area)
    if area.reset.num <= #area.reset.data then
        local data = area.reset.data[area.reset.num]
        local pos = data.pos
        local id = data.id
        Block:setBlockAll(pos.x, pos.y, pos.z, BLOCK_NORMAL)
        area.reset.num = area.reset.num + 1
    else
        for i = 1, #area.puzzle.flats do
            updateDigitalPrompt(area, area.puzzle.flats[i], i)
        end
        local resetP = LEVEL_CONFIG[area.index].RESET.POS
        local ret, data = Block:getBlockData(resetP.x, resetP.y, resetP.z)
        if data >= 8 then
            data = data - 8
        end
        Block:setBlockAll(resetP.x, resetP.y, resetP.z, BLOCK_SWITCH, data)
        area.isResetting = false
    end
end

function initAreaAttributes(areaPos)
    local attributes = {}
    local p1 = areaPos[1]
    local p2 = areaPos[2]

    attributes.srcPos = areaPos

    attributes.curPos = { POSITION(p1.x, p1.y, p1.z), POSITION(p2.x, p2.y, p2.z) }

    updateAreaSize(attributes)

    updateAreaFlats(attributes)

    attributes.midValues = {}
    local int, _ = math.modf((attributes.flats[1][1].z+ attributes.flats[1][2].z)/2)
    table.insert(attributes.midValues, int)
    int, _ = math.modf((attributes.flats[2][1].z+ attributes.flats[2][2].z)/2)
    table.insert(attributes.midValues, int)
    int, _ = math.modf((attributes.flats[3][1].x+ attributes.flats[3][2].x)/2)
    table.insert(attributes.midValues, int)
    int, _ = math.modf((attributes.flats[4][1].x+ attributes.flats[4][2].x)/2)
    table.insert(attributes.midValues, int)

    return attributes;
end

function initializeLevel()
    for i = 1, #LEVEL_CONFIG do
        local config = LEVEL_CONFIG[i]
        local area = {}
        area.index = i

        area.puzzle = initAreaAttributes(config.PUZZLE)
        area.answer = initAreaAttributes(config.ANSWER)

        area.state = 0

        area.separation = {}
        area.separation.times = {}
        area.separation.history = {}

        area.num = 0
        area.reset = {}
        area.reset.data = {}
        for x = area.puzzle.curPos[1].x, area.puzzle.curPos[1].x+area.puzzle.size.length-1 do
            for y = area.puzzle.curPos[1].y, area.puzzle.curPos[1].y+area.puzzle.size.height-1 do
                for z = area.puzzle.curPos[1].z, area.puzzle.curPos[1].z+area.puzzle.size.width-1 do
                    local ret, id = Block:getBlockID(x, y, z)
                    table.insert(area.reset.data, {id = id, pos = POSITION(x, y, z)})
                    if id ~= BLOCK_AIR then
                        area.num = area.num + 1
                    end
                end
            end
        end

        for j = 1, #area.answer.flats do
            updateDigitalPrompt(area, area.answer.flats[j], j)
            updateDigitalPrompt(area, area.puzzle.flats[j], j)
        end

        if config.TRANSFER then
            local tr = config.TRANSFER.POS
            local _, id = Block:getBlockID(tr.x, tr.y, tr.z)
            if id == BLOCK_ACTIVE then
                Block:setBlockAll(tr.x, tr.y, tr.z, BLOCK_AIR)
            end
        end

        table.insert(areas, area)
    end

end

function listenevents_minesweeper()
    --local ScriptSupportEvent = class.ScriptSupportEvent.new()

    local listenBlocks = { { BLOCK_NORMAL }, { BLOCK_FLOOR }
    , {668}, {669}, {670}
    , {671}, {672}, {673}
    , {674}, {675}, {676}
    , {677}, {678}, {679}
    , {680}, {681}, {682}
    , {1080}, { BLOCK_ACTIVE }, {724} }
    for i = ID_prompts.min, ID_prompts.max do
        table.insert(listenBlocks, {i})
    end

    ScriptSupportEvent:registerEvent("Game.Start", minesweeper_game_started)

    ScriptSupportEvent:registerEvent("Game.Run", minesweeper_game_run)

    ScriptSupportEvent:registerEvent('Actor.Projectile.Hit', minesweeper_actor_projectilehit)

    ScriptSupportEvent:registerEvent_Block('Block.DestroyBy', minesweeper_block_destroyedby, listenBlocks)

    ScriptSupportEvent:registerEvent_Block('Block.Trigger', minesweeper_block_trigger, listenBlocks)

    ScriptSupportEvent:registerEvent("Player.PickUpItem", minesweeper_player_pickupitem)

    ScriptSupportEvent:registerEvent("Actor.Die", minesweeper_monster_dead)

    ScriptSupportEvent:registerEvent("Player.SelectShortcut", minesweeper_player_selectshortcut)

    --ScriptSupportEvent:listenEventList()
end

minesweeper_game_started = function()
    sendSystemMsg("Stone Code - You wake up in a mysterious maze and a boy stands in front of you. ")
    local ret, num, array = World:getAllPlayers()
    initializeLevel()
end

minesweeper_game_run = function()
    if #errorPoints > 0 then
        local curTime = os.time()
        local i = 1
        while i <= #errorPoints do
            local ep = errorPoints[i]
            if curTime - ep.time > ERROR_TIME then
                local ret, id = Block:getBlockID(ep.pos.x, ep.pos.y, ep.pos.z)
                if id == BLOCK_ERROR then
                    Block:setBlockAll(ep.pos.x, ep.pos.y, ep.pos.z, BLOCK_NORMAL)
                end
                table.remove(errorPoints, i)
            else
                break
            end
        end
    end

    for i = 1, #areas do
        if areas[i].isResetting then
            resetting(areas[i])
        end
    end

end

minesweeper_actor_projectilehit = function(params)
    local blockid, x, y, z, eventobjid = params.blockid, params.x, params.y, params.z, params.eventobjid;
    local text = string.format("Hit %d,%d,%d,%d",blockid, x, y, z)
    local tool = TItem:getItemID(eventobjid)
    local dir
    TItem:destroyProjectile(eventobjid)
    local ret, data = Block:getBlockData(x, y, z)
    local bIsPrompt = false

    local pos = POSITION(x, y, z)
    local side
    if blockid ~= BLOCK_NORMAL then
        side = isPrompt(blockid)
        if side ~= 0 then
            local config = { {'x', 1}, {'x', -1}, {'z', 1}, {'z', -1}, {'y', 1}, {'y', -1} }
            if side == 1 then
                local ret, data = Block:getBlockData(x, y, z)
                local face = { 4, 1, 2, 3 }
                for i = 1, #face do
                    if data == face[i] then
                        pos[config[i][1]] = pos[config[i][1]] + config[i][2]
                        dir = i
                        break
                    end
                end
            elseif side == 2 then
                pos[config[5][1]] = pos[config[5][1]] + config[5][2]
            elseif side == 3 then
                pos[config[6][1]] = pos[config[6][1]] + config[6][2]
            end
            local ret, id = Block:getBlockID(pos.x, pos.y, pos.z)
            bIsPrompt = true
            blockid = id
        end
    end

    local area
    for i = 1, #areas do
        if isInArea(pos, areas[i].puzzle) then
            area = areas[i]
            if bIsPrompt and dir then

            else
                for j = 1, #area.puzzle.flats do
                    if isInFlat(pos, area.puzzle.flats[j]) then
                        local flat = area.puzzle.flats[j]
                        dir = j
                        if dir >= 5 then
                            dir = dir - 3
                        end
                        break
                    end
                end
            end

            break
        end
    end

    if not area then
        return
    elseif area.isResetting or area.state ~= 0 then
        return
    end

    if tool == TOOLS_ID_SEPARATE and dir then
        separate(area, pos, dir)
    elseif isInclude(tool, TOOLS_ID_DYE) then
        if blockid == BLOCK_NORMAL then
            dye(area, pos, tool)
        end
    elseif tool == TOOLS_ID_DIG then
        if blockid == BLOCK_NORMAL then
            Block:destroyBlock(pos.x, pos.y, pos.z, false)
            if verifySingleBlockInAnswerArea(area, pos) then
                updateSidesDigitalPrompt(pos)
                checkCompletion(area)
            else
                setErrorBlock(pos)
            end
        end
    end

end

minesweeper_block_destroyedby = function(param)
    local blockid, x, y, z = param.blockid, param.x, param.y, param.z;
    local area
    for i = 1, #areas do
        if isInArea(POSITION(x, y, z), areas[i].puzzle) then
            area = areas[i]
            break
        end
    end

    if not area then
        local data = 0
        if blockid == BLOCK_SWITCH then
            local axes = { 'x', 'x', 'z', 'z' }
            local values = { 1, -1, 1, -1 }
            local toFace = { 1, 0, 3, 2 }
            for i = 1, #axes do
                local pos = POSITION(x, y, z)
                pos[axes[i]] = pos[axes[i]] + values[i]
                local ret, id = Block:getBlockID(pos.x, pos.y, pos.z)
                if id ~= BLOCK_AIR then
                    data = toFace[i]
                    break
                end
            end
        end
        Block:setBlockAll(x, y, z, blockid, data)
        return
    end

    if blockid ~= BLOCK_NORMAL or area.isResetting then
        if area.isResetting then
            blockid = BLOCK_NORMAL
        end
        Block:setBlockAll(x, y, z, blockid)
    else
        if verifySingleBlockInAnswerArea(area, POSITION(x, y, z)) then
            updateSidesDigitalPrompt(POSITION(x, y, z))
        end
    end
end

minesweeper_monster_dead = function(params)
    local objid = params.eventobjid
    local toobjid = params.toobjid
    for i = 1, #areas do
        local area = areas[i]
        if area.monsterids then
            local index
            for j = 1, #area.monsterids do
                if objid == area.monsterids[j] then
                    index = j
                    break
                end
            end
            if index then
                table.remove(area.monsterids, index)
                if #area.monsterids == 0 then
                    if LEVEL_CONFIG[area.index].TRANSFER then
                        local tr = LEVEL_CONFIG[area.index].TRANSFER.POS
                        --Block:setBlockAll(tr.x, tr.y, tr.z, BLOCK_ACTIVE)
                        --sendSystemMsg("A new portal is activated. ")
                    end
                end
            end
        end
    end

end

minesweeper_block_trigger = function(params)
    local blockid, x, y, z, eventobjid = params.blockid, params.x, params.y, params.z, params.eventobjid;
    local ret, data = Block:getBlockData(x, y, z)
    for i = 1, #areas do
        local area = areas[i]
        local resetP = LEVEL_CONFIG[i].RESET.POS
        if data >= 8 and x == resetP.x and y == resetP.y and z == resetP.z then
            if not area.isResetting then
                startReset(area)
            end
            break
        end
    end
    Player:notifyGameInfo2Self(eventobjid, "Successfully restored ")
end

minesweeper_player_pickupitem = function(params)
    local itemid, itemnum, toobjid, eventobjid = params.itemid, params.itemnum, params.toobjid, params.eventobjid
    if not isInclude(itemid, ALLOWED_ITEMS) then
        Backpack:removeGridItemByItemID(eventobjid, itemid)
    else

    end
end

minesweeper_player_selectshortcut = function(params)
    local itemid, itemnum, eventobjid = params.itemid, params.itemnum, params.eventobjid
    for i = 1, #TOOLS_STRING do
        if TOOLS_STRING[i].id == itemid then
            Player:notifyGameInfo2Self(eventobjid, TOOLS_STRING[i].string)
            break
        end
    end
end

listenevents_minesweeper()
Last Update: 9/9/2019, 5:57:21 PM