• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

Divine Roguelike v16.2b - Save/Load system added for multiplayer

Rogue - is an adventure roguelike where the player (players) with their squads travel through locations.

Discord discord.com/invite/Y2zQ8Eqakg

Everything is as it should be according to the canons of roguelikes: heroes, creeps, locations, items, quests are generated.
And after death, there is a permanent pumping of the soul, which strengthens all new heroes with each of their revivals.
Do not be afraid to die (you will have to do this often), since death is a component of the improvement of heroes.

Gameplay maps from Archimond7450 (!new version map!):

Gameplay maps from WTii (old version map):

Gameplay maps from other authors (old version map):
Version 6.0
Version 6.0
Version 6.0

Gameplay maps from HushBook (old version map):

Features of the current versio
  • Randomization of enemy groups. The further the player goes, the harder it get
  • Choice of direction. After clearing the location, the player is given a choice of one of the following 4 location
  • Shower improvement. The player can upgrade the souls of their heroes so that they are stronger the next time they run. For example, by improving greed, heroes at the start in the next life will have more gold, and improving charisma from the next life increases the combat performance of mercenaries. You can even buy additional souls to increase the number of heroes you can control at the same tim
  • Village improvement. At a certain stage, a village will open for players, the development of which will give bonuses not only to the current passage, but also to all future race
  • Hiring troops. A distinctive feature of this roguelike is that the game offers to manage squads, and not single heroes. Lead huge troops against enemy armies at high level
  • Don't be afraid of losses. After cleaning each group, all your troops are reborn. This gameplay decision was made so that players are not afraid to engage in difficult battles that portend heavy losse
  • save/load. Use the /save and /load commands to save your souls and transfer your progress to the map of the next versions as well.
Previews
Contents

Divine Roguelike v15.12 (Map)

Divine Roguelike v16.2b (Map)

Reviews
deepstrasz
In the village, the red circle of power is named in Russian. Same with the gold item. The map is quite heavily dependent on Warcraft III standard material. The Razormane Brute has its description missing - tooltip missing. Progression to higher levels...
Level 8
Joined
Feb 20, 2015
Messages
58
There's a difference between the username and the map author. Can you prove you are indeed the author. If not, do you have permission to upload?
This is my map. I started to make it in Russian, but I had to translate it into English, as many players requested it. Here is the page with her changes. I have all versions of the map. I can send any to you to make sure. Rogue-like Map - WarCraft 3 / Моддинг - XGM
U can send me latter in box [email protected]
 

deepstrasz

Map Reviewer
Level 73
Joined
Jun 4, 2009
Messages
19,679
What do I need to do to update the map to version 0.5.10? Re-upload as a separate map, or is it possible to change the map in this post? Unfortunately I can't figure it out.
Do not multiupload please. Update like so: Updating resources

Also do not multipost.
Please use the yellow Edit button at the bottom of your posts and add text there instead of writing one post after another of yours unless it's an important update.
 
Level 8
Joined
Feb 20, 2015
Messages
58
Unfortunately, I could not find the file editing interface, although I searched for a long time.
So I'm posting an update in the comments. V 0.5.15 [en]
Lots of bugs fixed here
fixed some leaks
the game is smoother
more locations supported.
Added hidden events.
Partially corrected balance.
Added enemy camps with enemy troops and heroes.
1673286606462.png

v10 - 4.png
v10 - 5.png
 
Last edited:
Level 8
Joined
Feb 20, 2015
Messages
58
It's there at the bottom of the description as shown in the pictures. Please take some time to find it.
Thank you for your patience! I eventually found where it is.

0.5.
Fixed some bugs and freeze
Added a sanctuary where heroes go after death.
The sanctuary is a separate island of the dead, where players can make a number of improvements to their soul before moving into new heroe
Added a lot of gameplay hint
The difficulty of the game has been adjusted.

изображение_2023-01-11_015716583.png
изображение_2023-01-11_015739648.png
 
Last edited:
Level 1
Joined
Jan 11, 2023
Messages
1
Been playing with people on my chat and quick suggestions here:

  • Make Attribute Bonus (+1, +2, +3 for all) after lvl 10. Helps a lot on late game.
  • Save system for your progress RPG style, helps for other runs and lobbies
 
Level 8
Joined
Feb 20, 2015
Messages
58
Been playing with people on my chat and quick suggestions here:

  • Make Attribute Bonus (+1, +2, +3 for all) after lvl 10. Helps a lot on late game.
  • Save system for your progress RPG style, helps for other runs and lobbies
Thanks for the feedback!

* The skill system will be completed. After level 10, it is planned to give the player the opportunity to strengthen one of their abilities at the runemaster in exchange for resetting to level 1.
Maybe I will add +1 growth to the characteristics, but for this, the monsters at the late stage will also need to be strengthened, since now the monsters have nothing to counter the 15+ levels of heroes.

* The save system needs to be done, yes. I will need to take the time to figure out how to make such a system. And implement it.
 

deepstrasz

Map Reviewer
Level 73
Joined
Jun 4, 2009
Messages
19,679
In the village, the red circle of power is named in Russian. Same with the gold item.
The map is quite heavily dependent on Warcraft III standard material.
The Razormane Brute has its description missing - tooltip missing.
Progression to higher levels is slow. You barely can gather some lumber to upgrade after you die and you always start on level 1.
The game gets repetitive quickly.

It's nice but could use more gameplay variety and things to do.

Approved.
 
Level 8
Joined
Feb 20, 2015
Messages
58
Updated to version 0.6.5
  • A new cool member has joined the project. Programmer.
  • in connection with the previous paragraph, the first wave of refactoring was carried out.
  • significant optimization in many aspects of the game
  • new locations, abilities
  • minor rebalance of some in-game values
  • fixed Russian text in some places * added city level design
  • English texts are corrected by a member who has a good command of the language. Now the texts should be more understandable.
 
Level 8
Joined
Jan 23, 2015
Messages
123
Yeah, that new dev is me -- played this on battlenet, loved it and asked to help. Map is obviously barebones/WIP, but core gameplay is already fun and addictive, and we've made a lot of progress this week. I see many positive responses from the players, which empowers to keep up the development pace.
Any updates on Save / Load system for Multiplayer?
We're a group of friends just waiting for that :d
I've got a save-load system in testing phase, should be good in a few days
 

deepstrasz

Map Reviewer
Level 73
Joined
Jun 4, 2009
Messages
19,679
Yeah, that new dev is me -- played this on battlenet, loved it and asked to help. Map is obviously barebones/WIP, but core gameplay is already fun and addictive, and we've made a lot of progress this week. I see many positive responses about the map, which empowers to keep up the development pace.
Great. Don't like to advertise but you definitely use this map as inspiration: Warrogue_v1.10b
There are certainly similar others to play to get ideas from but I can't really remember their names right now.
 
Level 8
Joined
Feb 20, 2015
Messages
58
Great. Don't like to advertise but you definitely use this map as inspiration: Warrogue_v1.10b
Actually, no. Your assumption is wrong. This map was inspired by classic roguelikes such as UnderMine, Hand of Fate 2, Wizard of Legends, Slay the Spire, Dungered, Cult of the Lamb and more.

I haven't played Warcraft in 8 years since university. I was inspired that Blizzard made new updates to Warcraft 3, and I decided to make a map in my favorite genre. So I have no idea what other maps are out there right now. I'm a game designer, and in my day job we're also working on a few roguelikes right now.

I didn't set out to make custom heroes, custom units, etc. as I personally don't see the point in making custom content just for the sake of custom content. As for me, the classic balance of Blizzard is quite suitable for the implemented mechanics. And it was better to spend time on the implementation of the roguelike mechanics than on custom ones, which are not the fact that they would be more fun than the original.

But this does not mean that custom will not appear in the future. This means that the implementation of custom heroes is a low priority, compared to the implementation of new mechanics and interface, which are much higher priorities.

Raw product is a relative concept. Games can be upgraded indefinitely. As for me, if you often come across random players with whom the game lasts 3-8 continuous hours, and they leave positive feedback, then from a gameplay point of view, the game is no longer raw. There is just always room for improvement.

Would you perhaps have a Discord? I've got a few ideas, but moderation approval wait time are annoying me :d
Great idea! I will add a discord server. I will be glad if you join via the link Join the WC3 - Rogue-like Map Discord Server!


Updated to version 0.6.7
  • Fixed some bugs Added
  • New location - "Test Tower". Accept one of the challenges (from easy to hard), hold back the hordes of enemies and get the corresponding reward! Note: on the screen part of the text is in Russian, these are the names of objects. You will have them called in the language in which the game is installed.
WC3ScrnShot_011623_134424_000.png
 
Last edited:
Level 8
Joined
Feb 20, 2015
Messages
58
I am sorry. I seem to have eaten a word, namely "could". I wanted to mean that the map is good for inspiration not that yours is based on it.
Thanks for the clarification! References will never be superfluous. Somehow I'll get to this map.

Update
* Fixed several important bugs that, under certain circumstances, could block the passage.
 
Last edited:
Level 8
Joined
Feb 20, 2015
Messages
58
Updated to version 0.7.12

  • added the ability to improve some of the hero's abilities in higher educational institutions, which can be found at the starting location and in the city. So far, damage-dealing abilities have been improved. In a subsequent iteration, the ability to increase the ability to summon will be added.
  • added the ability to switch to possible actions when closing the portal to the 16th level of danger of the world.
  • many minor fixes

This update was supposed to keep the system for multiplayer, but this system is still taking on some of the bugs, so its implementation is still delayed.
 
Level 4
Joined
May 11, 2020
Messages
43
I played this map a bunch yesterday and it's obviously unfinished, some things are not implemented and a bunch of the mercenaries have tooltips missing. It is SO DAMN FUN though! Really excited to see it grow and improve, good job author.
 
Level 8
Joined
Feb 20, 2015
Messages
58
I played this map a bunch yesterday and it's obviously unfinished, some things are not implemented and a bunch of the mercenaries have tooltips missing. It is SO DAMN FUN though! Really excited to see it grow and improve, good job author.
Thank you! We try.

Also, fyi this link is expired now
Yes, I screwed up. Here is a perpetual link.
 
Level 8
Joined
Feb 20, 2015
Messages
58
Gameplay maps from WTii:





Our team felt an influx of motivation for further work.
We are currently trying to sort out synchronization issues.
The map already has a save and load system for multiplayer.
However, this system is disabled, as it causes desynchronization if players play in different languages.
We can't figure out what it is yet.
Maybe there is someone here who is willing and able to help with finding the problem? The triggers responsible for saving are located in the folder saveload.
1676817715863.png

Lua:
if Debug then Debug.beginFile "Base64" end
--[[
    Base64 v1

    Provides functionality to tightly pack data, optimized for the most dense info storage.

    API:

        Base64.Encoder.create() -> Encoder
            - Creates a new encoder instance

        Encoder:writeBitString(bitString: integer, bitLength: integer)
            - Add bitLength bits to the resulting data

        Encoder:buildString() -> string
            - Returns a string with the encoded data
    
        Base64.Decoder.create(data: string) -> Decoder
            - Creates a decoder instance that will read through the string
        
        Decoder:readBitString(bitLength: integer) -> integer
            - Reads the next bitLength bits from the 

        Base64.Internal
            CHARMAP - the default RFC4822 compliant charmap
            REVERSE_CHARMAP - the inverse of the default charmap

            GenerateCharmap(charset: string, voidInt, setSize, i2ch, ch2i) -> Charmap, ReverseCharmap
                - Generates a charmap and its reverse for the given charset
                - Can be used to obfuscate the encoding
    
    Optional requirements:
        DebugUtils by Eikonium                          @ https://www.hiveworkshop.com/threads/330758/
        Total Initialization by Bribe                   @ https://www.hiveworkshop.com/threads/317099/

    Inspired by:
        - 's Base64         @ https://www.hiveworkshop.com/threads/278664/
        - Aniki's Base64 & BitBuf   @ https://www.hiveworkshop.com/threads/325749/

    Updated: 18 Jan 2023
--]]
OnInit("Base64", function()
    -- precomputed charmaps
    local CHARMAP = {
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P",
        "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f",
        "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
        "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"
    }
    local REVERSE_CHARMAP = {
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54,
        55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2,
        3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
        20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30,
        31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
        48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
    }

    --- Generates a new charset dictionaries
    ---@param charset string
    local function GenerateCharmap(charset, voidInt, setSize, i2ch, ch2i)
        i2ch = i2ch or {}
        ch2i = ch2i or {}
        voidInt = voidInt or 1
        setSize = setSize or 255
        for i = 1, setSize, 1 do
            ch2i[i] = voidInt
        end
        for i = 1, #charset, 1 do
            i2ch[i] = charset:sub(i, i)
            ch2i[charset:byte(i, i)] = i
        end
        return i2ch, ch2i
    end

    local Internal = {
        CHARMAP,
        REVERSE_CHARMAP,
        GenerateCharmap = GenerateCharmap,
    }

    --[ ENCODER CLASS ]--

    --- Encodes a integer value sequence into a string.
    --- Supports arbitrary bit lengths.
    ---@class Encoder
    ---@field buffer integer
    ---@field buffer_len integer
    ---@field bit_len integer
    ---@field out table
    ---@field out_len integer
    local Encoder = {}
    Encoder.__index = Encoder

    function Encoder.create()
        return setmetatable({
            buffer = 0,
            buffer_len = 0,
            bit_len = 0,
            out = {},
            out_len = 0
        }, Encoder)
    end

    --- Must be sure that the integer provided is in the range [0, 64]
    ---@param e Encoder
    ---@param octet integer
    local function writeOctetUnsafe(e, octet)
        e.out_len = e.out_len + 1
        e.out[e.out_len] = CHARMAP[octet + 1]
    end

    ---@param e Encoder
    ---@param buffer integer
    local function writeBuffer(e, buffer)
        writeOctetUnsafe(e, (buffer & 0x3f))
        writeOctetUnsafe(e, (buffer & 0xfc0) >> 6)
        writeOctetUnsafe(e, (buffer & 0x3f000) >> 12)
        writeOctetUnsafe(e, (buffer & 0xfc0000) >> 18)
    end

    ---@param e Encoder
    ---@param buffer integer
    ---@param len integer
    local function addToBuffer(e, buffer, len)
        if len < 24 then
            return buffer, len
        end
        local r, l = 0, 0
        if len > 24 then
            l = len - 24
            r = buffer & ((1 << l) - 1)
            buffer = buffer >> l
        end
        writeBuffer(e, buffer)
        return r, l
    end

    --- Must be sure that the len is in the range [1,31], and the integer is in [0, 2^len - 1]
    ---@param bstr integer
    ---@param len integer
    function Encoder:writeBitString(bstr, len)
        -- optimised for performance
        -- bstr = bstr & (1 << len) - 1) -- clamp extra bits for safety -- isn't necessary for Object64 lib
        self.bit_len = self.bit_len + len
        local shift = self.buffer_len + len
        if shift <= 31 then
            self.buffer, self.buffer_len = addToBuffer(self, (self.buffer << len) | bstr, shift)
            return
        end
        -- self.buffer <= 23, len <= 31 -> self.buffer + len == 54 in worst case scenario
        local rem = shift - 24
        writeBuffer(self, (self.buffer << (24 - self.buffer_len)) | (bstr >> rem))
        bstr = bstr & ((1 << rem) - 1)
        if rem > 24 then
            rem = rem - 24
            writeBuffer(self, bstr >> rem)
            bstr = bstr & ((1 << rem) - 1)
        end
        self.buffer, self.buffer_len = addToBuffer(self, bstr, rem)
    end

    function Encoder:buildString()
        if self.buffer > 0 then
            writeBuffer(self, self.buffer << (24 - self.buffer_len))
        end
        return table.concat(self.out)
    end

    --[ DECODER CLASS ]--
    ---@class Decoder
    ---@field buffer integer
    ---@field buffer_len integer
    ---@field bit_ptr integer
    ---@field pointer integer
    ---@field source string
    local Decoder = {}
    Decoder.__index = Decoder

    function Decoder.create(str)
        return setmetatable({
            buffer = 0,
            buffer_len = 0,
            bit_ptr = 0,
            pointer = 0,
            source = str
        }, Decoder)
    end

    ---@param e Decoder
    local function readOctetUnsafe(e)
        if e.pointer >= #e.source then
            return 0
        end
        local value = REVERSE_CHARMAP[string.byte(e.source, e.pointer + 1, e.pointer + 1)]
        e.pointer = e.pointer + 1
        return value
    end

    --- Reads the next 24 bits
    ---@param e Decoder
    local function readBuffer(e)
        local r = readOctetUnsafe(e)
        r = r | readOctetUnsafe(e) << 6
        r = r | readOctetUnsafe(e) << 12
        r = r | readOctetUnsafe(e) << 18
        return r
    end

    ---@param e Decoder
    local function readToBuffer(e, buffer, len)
        if len > 7 then
            return buffer, len
        end
        return (buffer << 24) | readBuffer(e), len + 24
    end

    ---@param len integer
    function Decoder:readBitString(len)
        -- optimised for performance
        -- reversing the operations in writeBitString
        local shift = len - self.buffer_len
        if shift < 0 then
            shift = -shift
            local value = self.buffer >> shift
            self.buffer, self.buffer_len = readToBuffer(self, self.buffer & ((1 << shift) - 1), shift)
            return value
        end
        local value = self.buffer << shift
        local bstr = readBuffer(self, 0, 0)
        if shift > 24 then
            shift = shift - 24
            value = value | (bstr << shift)
            bstr = readBuffer(self, 0, 0)
        end
        shift = 24 - shift
        value = value | (bstr >> shift)
        self.buffer, self.buffer_len = readToBuffer(self, bstr & ((1 << shift) - 1), shift)
        return value
    end

    Base64 = {
        Encoder = Encoder,
        Decoder = Decoder,
        Internal = Internal,
    }

    local function test()
        local e = Encoder.create()
        e:writeBitString(0x1234567, 28)
        e:writeBitString(0x1234567, 28)
        e:writeBitString(0xf, 4)
        e:writeBitString(0x1234567, 28)
        e:writeBitString(0x1234567, 28)

        local d = Decoder.create(e:buildString())
        d:readBitString(28)
        d:readBitString(28)
        d:readBitString(4)
        d:readBitString(28)
        d:readBitString(28)
    end

    -- Internal.CHARMAP, Internal.REVERSE_CHARMAP = GenerateCharmap(
    --     "yJ3uFjRLC0h5NTSMYHm27WOGUI/Q+9rKiDPdagtsABpxlwoce48nkzXqvE1ZbfV6"
    -- )
end)
if Debug then Debug.endFile() end
Lua:
if Debug then Debug.beginFile "FileIO" end
--[[
    FileIO v1

    Provides functionality to read and write files, optimized with lua functionality in mind.

    API:

        FileIO.Save(filename, data)
            - Write string data to a file
    
        FileIO.Load(filename) -> string
            - Read string data from a file

        FileIO.SaveAsserted(filename, data, onFail?) -> bool
            - Saves the file and checks that it was saved successfully.
              If it fails, passes (filename, data, loadResult) to onFail.

        FileIO.enabled : bool
            - field that indicates that files can be accessed correctly.
    
    Optional requirements:
        DebugUtils by Eikonium                          @ https://www.hiveworkshop.com/threads/330758/
        Total Initialization by Bribe                   @ https://www.hiveworkshop.com/threads/317099/

    Inspired by:
        - TriggerHappy's Codeless Save and Load         @ https://www.hiveworkshop.com/threads/278664/
        - ScrewTheTrees's Codeless Save/Sync concept    @ https://www.hiveworkshop.com/threads/325749/

    Updated: 18 Jan 2023
--]]
OnInit("FileIO", function()
    local RAW_PREFIX = ']]i[['
    local RAW_SUFFIX = ']]--[['
    local RAW_SIZE = 256 - #RAW_PREFIX - #RAW_SUFFIX
    local LOAD_ABILITY = FourCC('ANdc')

    local function open(filename)
        name = filename
        PreloadGenClear()
        Preload('")\nendfunction\n//!beginusercode\nlocal p={}local i=function(s)table.insert(p,s)end--[[')
    end

    local function write(s)
        for i = 1, #s, RAW_SIZE do
            Preload(RAW_PREFIX .. s:sub(i, i + RAW_SIZE - 1) .. RAW_SUFFIX)
        end
    end

    local function close()
        Preload(']]BlzSetAbilityTooltip(' ..
            LOAD_ABILITY .. ',table.concat(p),0)\n//!endusercode\nfunction a takes nothing returns nothing\n//')
        PreloadGenEnd(name)
        name = nil
    end

    ---
    ---@param filename string
    ---@param data string
    local function savefile(filename, data)
        open(filename)
        write(data)
        close()
    end

    ---@param filename string
    ---@return string?
    local function loadfile(filename)
        local s = BlzGetAbilityTooltip(LOAD_ABILITY, 0)
        BlzSetAbilityTooltip(LOAD_ABILITY, '', 0)
        Preloader(filename)
        local loaded = BlzGetAbilityTooltip(LOAD_ABILITY, 0)
        BlzSetAbilityTooltip(LOAD_ABILITY, s, 0)
        if loaded == s then
            return nil
        end
        return loaded
    end

    ---@param filename string
    ---@param data string
    ---@param onFail function?
    ---@return boolean
    local function saveAsserted(filename, data, onFail)
        savefile(filename, data)
        local res = loadfile(filename)
        if res == data then
            return true
        end
        if onFail then
            onFail(filename, data, res)
        end
        return false
    end

    local fileIO_enabled = saveAsserted('TestFileIO.pld', 'FileIO is Enabled')

    FileIO = {
        Save = savefile,
        Load = loadfile,
        SaveAsserted = saveAsserted,
        enabled = fileIO_enabled,
    }
end)
if Debug then Debug.endFile() end
Lua:
if Debug then Debug.beginFile "Object64" end
OnInit("Object64", function(require)

    function log2(num)
        local i = 0
        while num > 0 do
            i = i + 1
            num = num >> 1
        end
        return i
    end

    local Field = {}

    --[[  FIELDS:  ]]

    --[[----------------------]]
    --[[   Unsigned Integer   ]]
    --[[----------------------]]

    ---@class Field.UInt
    ---@field name string
    ---@field bit_len integer
    Field.UInt = {}
    Field.UInt.__index = Field.UInt

    function Field.UInt.new(name, maxValue)
        return setmetatable({
            name = name,
            bit_len = (maxValue and log2(maxValue)) or 31
        }, Field.UInt)
    end

    ---@param decoder Decoder
    function Field.UInt:decode(decoder)
        return decoder:readBitString(self.bit_len)
    end

    ---@param encoder Encoder
    function Field.UInt:encode(encoder, value)
        encoder:writeBitString(value, self.bit_len)
    end

    --[[----------------------]]
    --[[       Boolean        ]]
    --[[----------------------]]

    ---@class Field.Bool
    ---@field name string
    ---@field bit_len integer
    Field.Bool = {}
    Field.Bool.__index = Field.Bool

    --- Accepts only dense arrays
    ---@param name string
    function Field.Bool.new(name)
        return setmetatable({
            name = name,
            bit_len = 1
        }, Field.Bool)
    end

    Field.Bool.Instance = Field.Bool.new('bool')

    ---@param decoder Decoder
    function Field.Bool:decode(decoder)
        local code = decoder:readBitString(1)
        return code ~= 0
    end

    ---@param encoder Encoder
    function Field.Bool:encode(encoder, value)
        encoder:writeBitString(value * 1, 1)
    end

    --[[----------------------]]
    --[[    Signed Integer    ]]
    --[[----------------------]]

    ---@class Field.SignedInt
    ---@field name string
    ---@field bit_len integer
    Field.SignedInt = {}
    Field.SignedInt.__index = Field.SignedInt

    function Field.SignedInt.new(name, maxValue)
        return setmetatable({
            name = name,
            bit_len = 1 + ((maxValue and log2(maxValue)) or 31)
        }, Field.SignedInt)
    end

    ---@param decoder Decoder
    function Field.SignedInt:decode(decoder)
        local value = decoder:readBitString(self.bit_len)
        if Field.Bool.Instance:decode(decoder) then
            return -value
        end
        return value
    end

    ---@param encoder Encoder
    function Field.SignedInt:encode(encoder, value)
        if value < 0 then
            encoder:writeBitString(-value, self.bit_len)
            Field.Bool.Instance:encode(decoder, true)
        else
            encoder:writeBitString(value, self.bit_len)
            Field.Bool.Instance:encode(decoder, false)
        end
    end

    --[[----------------------]]
    --[[         Char         ]]
    --[[----------------------]]


    local chars = " !#$%%&'\"()*+,-.0123456789:;=<>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}`"
    local charset, charset_reverse = Base64.Internal.GenerateCharmap(chars)
    -- table.print(charset)
    -- table.print(charset_reverse)
    ---@class Field.Char
    ---@field name string
    ---@field bit_len integer
    ---@field parser ArrayParser
    Field.Char = {}
    Field.Char.__index = Field.Char
    Field.Char.Set = charset
    Field.Char.SetRef = charset_reverse
    Field.Char.UIntField = Field.UInt.new('char-uint', #chars)

    function Field.Char.new(name)
        return setmetatable({
            name = name,
            bit_len = Field.Char.UIntField.bit_len,
        }, Field.Char)
    end

    Field.Char.Instance = Field.Char.new('char')

    ---@param decoder Decoder
    function Field.Char:decode(decoder)
        local uint = Field.Char.UIntField:decode(decoder)
        return charset[uint + 1] or ' '
    end

    --- Accepts a byte value, not string
    ---@param encoder Encoder
    function Field.Char:encode(encoder, value)
        Field.Char.UIntField:encode(encoder, (charset_reverse[value] or 1) - 1)
    end

    --[[----------------------]]
    --[[       Value Set      ]]
    --[[----------------------]]

    --- Object field info that holds a value from a specific value set.
    ---@class Field.ValueSet
    ---@field name string
    ---@field bit_len integer
    ---@field ref table
    ---@field backref table
    Field.ValueSet = {}
    Field.ValueSet.__index = Field.ValueSet

    function Field.ValueSet.createBackRef(object)
        backreference = {}
        for i, v in ipairs(valueArray) do
            backreference[v] = i
        end
        return backreference
    end

    --- Accepts only dense arrays
    function Field.ValueSet.new(name, valueArray, backreference)
        return setmetatable({
            name = name,
            bit_len = log2(#valueArray),
            ref = valueArray,
            backref = backreference or Field.ValueSet.createBackRef(valueArray)
        }, Field.ValueSet)
    end

    ---@param decoder Decoder
    function Field.ValueSet:decode(decoder)
        local code = decoder:readBitString(self.bit_len)
        return self.ref[code]
    end

    ---@param encoder Encoder
    function Field.ValueSet:encode(encoder, value)
        local code = self.backref[value]
        if not code then
            -- return print('Error writing Field ' .. self.name .. ': bad value ' .. value)
        end
        encoder:writeBitString(code, self.bit_len)
    end

    -- -- e.g.
    -- local InverseBoolField = Field.ValueSet.new('inverse-bool', { true, false })
    -- InverseBoolField:encode(..., true)   -> 0
    -- InverseBoolField:encode(..., false)  -> 1
    -- InverseBoolField:decode(... => 0)         -> true
    -- InverseBoolField:decode(... => 1)         -> false

    --[[----------------------]]
    --[[     Array Parser     ]]
    --[[----------------------]]

    --- Array type info specifying value type and maximum capacity.
    ---@class ArrayParser
    ---@field minbit_len integer
    ---@field valueType Field.UInt
    ---@field length Field.UInt
    local ArrayParser = {}
    ArrayParser.__index = ArrayParser

    function ArrayParser.new(field, maxSize)
        local len = Field.UInt.new('length', maxSize)
        return setmetatable({
            bit_len = len.bit_len + (field.bit_len << len.bit_len),
            valueType = field,
            length = len
        }, ArrayParser)
    end

    ---@param encoder Encoder
    function ArrayParser:encode(encoder, array)
        -- print('array', self.valueType.name, 'length', #array)
        self.length:encode(encoder, #array)
        for _, val in ipairs(array) do
            -- print('array', self.valueType.name, _, val)
            self.valueType:encode(encoder, val)
        end
    end

    ---@param decoder Decoder
    function ArrayParser:decode(decoder)
        local length = self.length:decode(decoder)
        -- print('array', self.valueType.name, 'length', length)
        local array = {}
        for i = 1, length, 1 do
            array[i] = self.valueType:decode(decoder)
            -- print('array', self.valueType.name, i, array[i])
        end
        return array
    end

    --[[----------------------]]
    --[[        String        ]]
    --[[----------------------]]

    --- String field info based on the ArrayParser<Char>.
    ---@class Field.String
    ---@field name string
    ---@field bit_len integer
    ---@field parser ArrayParser
    ---@field char Field.Char
    Field.String = {}
    Field.String.__index = Field.String

    function Field.String.new(name, maxLen)
        local parser = ArrayParser.new(Field.Char.Instance, maxLen)
        return setmetatable({
            name = name,
            bit_len = parser.bit_len,
            parser = parser,
        }, Field.String)
    end

    Field.String.Instance = Field.String.new('string-instance')

    ---@param decoder Decoder
    function Field.String:decode(decoder)
        local array = self.parser:decode(decoder)
        return table.concat(array)
    end

    ---@param encoder Encoder
    ---@param value string
    function Field.String:encode(encoder, value)
        -- print('string len', value, #value)
        self.parser.length:encode(encoder, #value)
        for i = 1, #value, 1 do
            -- print('str', value:sub(i, i))
            Field.Char.Instance:encode(encoder, value:byte(i, i))
        end
    end

    --[[----------------------]]
    --[[       Object         ]]
    --[[----------------------]]

    --- Object field that holds another object.
    --- Requires a parser to return the value.
    ---@class Field.Object
    ---@field name string
    ---@field bit_len integer
    ---@field parser ObjectParser
    Field.Object = {}
    Field.Object.__index = Field.Object

    --- Accepts only a completed parser
    function Field.Object.new(name, parser)
        return setmetatable({
            name = name,
            bit_len = parser.bit_len,
            parser = parser,
        }, Field.Object)
    end

    ---@param decoder Decoder
    function Field.Object:decode(decoder)
        return self.parser:decode(decoder)
    end

    ---@param encoder Encoder
    function Field.Object:encode(encoder, value)
        self.parser:encode(encoder, value)
    end

    --[[----------------------]]
    --[[     Object Parser    ]]
    --[[----------------------]]

    --- Object type info specifying what fields to serialize.
    ---@class ObjectParser
    ---@field bit_len integer
    ---@field param table
    ---@field fieldQueue table
    ---@field constructor function
    ---@field postprocessor function
    local ObjectParser = {}
    ObjectParser.__index = ObjectParser

    ---@param fieldArray table? Field list
    ---@param ctor function? Function that creates the object when decoding
    ---@param postproc function? Function that creates the object when decoding
    function ObjectParser.new(fieldArray, ctor, postproc)
        local params, queue = {}, nil
        if fieldArray then
            for _, v in ipairs(fieldArray) do
                params[v.name] = v
            end
            queue = fieldArray
        else
            queue = {}
        end
        return setmetatable({
            bit_len = 0,
            param = params,
            fieldQueue = queue,
            constructor = ctor or function() return {} end,
            postprocessor = postproc or function(x) return x end
        }, ObjectParser)
    end

    function ObjectParser:addField(field)
        self.param[field.name] = field
        self.fieldQueue[#self.fieldQueue + 1] = field
        self.bit_len = self.bit_len + field.bit_len
        field.order = #self.fieldQueue
    end

    function ObjectParser:addFields(...)
        local input = table.pack(...)
        for i = 1, input.n do
            self:addField(input[i])
        end
    end

    ---@param encoder Encoder
    function ObjectParser:encode(encoder, object)
        for _, def in ipairs(self.fieldQueue) do
            local value = object[def.name]
            -- print('obj[', _, def.name, ']', value)
            def:encode(encoder, value)
        end
    end

    ---@param decoder Decoder
    function ObjectParser:decode(decoder)
        local object = self.constructor()
        for _, def in ipairs(self.fieldQueue) do
            object[def.name] = def:decode(decoder)
            -- print('obj[', _, def.name, ']', object[def.name])
        end
        return self.postprocessor(object)
    end

    --[[----------------------]]
    --[[ Object Switch Parser ]]
    --[[----------------------]]

    --- Conditional type info that serializes the object into different types
    --- depending on a single-type argument.
    ---@class SwitchParser
    ---@field argType table
    ---@field argCreator function
    ---@field typeDecider function
    local SwitchParser = {}
    SwitchParser.__index = SwitchParser

    ---@param argType table The type info of the argument object
    ---@param argCreator function Function to create the argument from any object
    ---@param typeDecider function Function to decide the object type by the argument
    function SwitchParser.new(argType, argCreator, typeDecider)
        return setmetatable({
            bit_len = 0,
            argType = argType,
            argCreator = argCreator,
            typeDecider = typeDecider,
        }, SwitchParser)
    end

    ---@param encoder Encoder
    function SwitchParser:encode(encoder, object)
        local arg = self.argCreator(object)
        self.argType:encode(encoder, arg)
        self.typeDecider(arg):encode(encoder, object)
    end

    ---@param decoder Decoder
    function SwitchParser:decode(decoder)
        local arg = self.argType:decode(decoder)
        return self.typeDecider(arg):decode(decoder)
    end

    --[[----------------------]]
    --[[  Polymorphic Parser  ]]
    --[[----------------------]]

    local PolymorphicParser = {}
    PolymorphicParser.__index = PolymorphicParser

    ---@param argType table The type info of the argument object
    ---@param classTypes table Array of class types that contain the
    ---@param typeParsers table Table that matches class type to class parser
    function PolymorphicParser.new(argType, classTypes, typeParsers)
        local typeField = Field.ValueSet.new('polymorphic-type', classTypes, typeParsers)
        return setmetatable(SwitchParser.new(
            valueSet,
            function(obj) return getmetatable(obj) end,
            function(type) return typeParsers[type] end
        ), PolymorphicParser)
    end
    
    setmetatable(PolymorphicParser, SwitchParser)

    --[[----------------------]]
    --[[      Final lib       ]]
    --[[----------------------]]

    function Serialize(parser, object)
        local encoder = Base64.Encoder.create()
        parser:encode(encoder, object)
        -- print(encoder.bit_len)
        return encoder:buildString()
    end

    function Deserialize(parser, str)
        local decoder = Base64.Decoder.create(str)
        return parser:decode(decoder)
    end

    Object64 = {
        ObjectParser = ObjectParser,
        ArrayParser = ArrayParser,
        Field = Field,
        Serialize = Serialize,
        Deserialize = Deserialize,
    }
end)
if Debug then Debug.endFile() end

Lua:
if Debug then Debug.beginFile "PlayerState" end
OnInit("PlayerState", function()
    local localP = GetLocalPlayer()
    local localId = GetPlayerId(localP)
    local playing = CreateForce()
    local playingCount = 0
    local multiplayer = false
    local onLeaveEvents = {}
    local playingInit = {}
    local playerName = {}
    local tagId = {}
    local function onPlayerLeave(func)
        onLeaveEvents[#onLeaveEvents + 1] = func
    end

    local function onPlayingInit(func)
        if playingInit then
            playingInit[#playingInit + 1] = func
        else
            ForForce(playing, function() func(GetEnumPlayer()) end)
        end
    end

    do
        local leave_trigger = CreateTrigger()
        local function playerLeaved()
            local p = GetTriggerPlayer()
            for i = 1, #onLeaveEvents, 1 do
                onLeaveEvents[i](p)
            end
        end

        OnInit.final(function()
            TriggerAddCondition(leave_trigger, Condition(playerLeaved))
            for i = 0, bj_MAX_PLAYERS - 1 do
                local p = Player(i)
                local id = GetPlayerId(p)
                if GetPlayerController(p) == MAP_CONTROL_USER
                    and GetPlayerSlotState(p) == PLAYER_SLOT_STATE_PLAYING then
                    ForceAddPlayer(playing, p)
                    playingCount = playingCount + 1
                    TriggerRegisterPlayerEvent(leave_trigger, p, EVENT_PLAYER_LEAVE)
                end
                --- BNet tag calculation
                local name = GetPlayerName(p)
                local bnetName, tag = name:match('([^#]+)#(%%d+)')
                if bnetName then
                    playerName[id] = bnetName
                    tagId[id] = tonumber(tag)
                else
                    playerName[id] = name
                    tagId[id] = 0
                end
                for i = 1, #playingInit, 1 do
                    playingInit[i](p)
                end
            end
            multiplayer = playingCount > 1
            playingInit = nil
        end)
        onPlayerLeave(function(p)
            ForceRemovePlayer(playing, p)
            playingCount = playingCount - 1
        end)
    end

    do
        local CURRENT_VERSION = 1
        local MAP_KEY = "rG!B@n"
        local SYNC_PREFIX = "PST"
        local pState = {}
        local function GetSaveLocation()
            return ([[roguelike/%%s/Save-%%d.pld]]):format(playerName[localId], tagId[localId])
        end

        local playerStateValidation = Object64.ObjectParser.new({
            Object64.Field.String.new('key', 6),
            Object64.Field.UInt.new('save_version', 10000),
            Object64.Field.UInt.new('bnetTag', 10000),
            Object64.Field.String.new('player', 32),
        }, nil, function(header)
            header.valid = header.key == MAP_KEY
                and header.player == playerName[localId]
                and header.bnetTag == tagId[localId]
                and header.save_version <= CURRENT_VERSION
--             print(([[Checking header:
-- key = %%s | ref: %%s
-- player = %%s | ref: %%s
-- bnetTag = %%d | ref: %%d
-- save_version = %%d | ref: %%d
--     -> %%s]]):format(
--     header.key, MAP_KEY,
--     header.player, playerName[localId],
--     header.bnetTag, tagId[localId],
--     header.save_version, CURRENT_VERSION,
--     header.valid
-- ))
            return header
        end)

        local playerStateInfo = Object64.ObjectParser.new({
            Object64.Field.String.new('key', 6),
            Object64.Field.UInt.new('save_version', 10000),
            Object64.Field.UInt.new('bnetTag', 10000),
            Object64.Field.String.new('player', 32),
            Object64.Field.Object.new('data', Object64.ObjectParser.new({
                Object64.Field.UInt.new('game_count', 8192),
                Object64.Field.UInt.new('lumber'),
                Object64.Field.UInt.new('souls', 3),
                Object64.Field.UInt.new('longevity', 256),
                Object64.Field.UInt.new('merc_dmg', 256),
                Object64.Field.UInt.new('merc_hp', 256),
                Object64.Field.UInt.new('start_gold', 256),
                Object64.Field.UInt.new('start_exp', 256),
            })),
        })

        local function CreatePlayerData()
            return {
                game_count = 1,
                run_count = 0,
                longevity = 0,
                merc_dmg = 0,
                merc_hp = 0,
                start_gold = 0,
                start_exp = 0,
                souls = 0,
            }
        end

        local function OnLoadPlayerState(p, data)
            local id = GetPlayerId(p)
            -- print(("Received %%s <- %%s"):format(data, playerName[id]))
            local newState = Object64.Deserialize(playerStateInfo, data)
            local data = newState.data
            pState[id].data = data
            data.game_count = data.game_count + 1
            SetPlayerState(p, PLAYER_STATE_RESOURCE_LUMBER,
                GetPlayerState(p, PLAYER_STATE_RESOURCE_LUMBER)
                + data.lumber)
            data.lumber = nil
            print(("Welcome back to your %%dth game, %%s"):format(data.game_count, playerName[id]))
            if not multiplayer then
                print("You are playing in sigleplayer; your progress will not be saved.")
            end
            for i = 1, data.souls do
                GroupAddUnit(
                    udg_select_hero_souls,
                    CreateUnit(p, FourCC("ewsp"),
                        GetRandomReal(GetRectMinX(gg_rct_selectHero2_Copy), GetRectMaxX(gg_rct_selectHero2_Copy)),
                        GetRandomReal(GetRectMinY(gg_rct_selectHero2_Copy), GetRectMaxY(gg_rct_selectHero2_Copy)),
                    bj_UNIT_FACING)
                )
            end
            -- table.print(PlayerState[id])
        end

        local syncTrigger = CreateTrigger()
        TriggerAddCondition(syncTrigger, Condition(function()
            OnLoadPlayerState(GetTriggerPlayer(), BlzGetTriggerSyncData())
        end))

        local function SavePlayerState()
            if not multiplayer then return end
            local state = pState[localId]
            state.key = MAP_KEY
            state.data.lumber = GetPlayerState(localP, PLAYER_STATE_RESOURCE_LUMBER)
            local data = Object64.Serialize(playerStateInfo, state)
            state.key = nil
            state.data.lumber = nil
            FileIO.Save(GetSaveLocation(), data)
            -- print("Your soul shall be remembered for ages...")
            -- print(("Saved %%s -> %%s"):format(data, GetSaveLocation()))
        end

        local function LoadPlayerState(p)
            if localP == p then
                local data = FileIO.Load(GetSaveLocation())
                if not data then return end
                -- print(("Sent %%s <- %%s"):format(data, GetSaveLocation()))
                local h = Object64.Deserialize(playerStateValidation, data)
                if h.valid then
                    BlzSendSyncData(SYNC_PREFIX, data)
                else
                    -- if h.key ~= MAP_KEY then
                    --     Debug.throwError(('Invalid PlayerState.'))
                    -- end
                    -- if h.save_version > CURRENT_VERSION then
                    --     Debug.throwError(('Your Current PlayerState was saved on a higher map version.'))
                    -- end
                    -- if h.player ~= playerName[localId] then
                    --     Debug.throwError(('This PlayerState belongs to another player!'))
                    -- end
                    -- if h.bnetTag ~= playerName[localId] and h.bnetTag ~= 0 then
                    --     Debug.throwError(('PlayerState belongs to another battle.net account!'))
                    -- end
                end
            end
        end

        onPlayingInit(function(p)
            local id = GetPlayerId(p)
            pState[id] = {
                save_version = CURRENT_VERSION,
                bnetTag = tagId[id],
                player = playerName[id],
                data = CreatePlayerData()
            }
            BlzTriggerRegisterPlayerSyncEvent(syncTrigger, p, SYNC_PREFIX, false)
            LoadPlayerState(p)
        end)

        PlayerState = {
            Save = SavePlayerState,
            -- Load = LoadPlayerState,
        }

        function PlayerState.OnNewRun()
            ForForce(playing, function()
                local data = pState[GetPlayerId(GetEnumPlayer())].data
                data.run_count = data.run_count + 1
            end)
        end

        GlobalRemapArray("udg_souls_upg_gold",
            function(i) return pState[i - 1].data.start_gold or 0 end,
            function(i, value) pState[i - 1].data.start_gold = value end
        )
        GlobalRemapArray("udg_souls_upg_xp",
            function(i) return pState[i - 1].data.start_xp or 0 end,
            function(i, value) pState[i - 1].data.start_xp = value end
        )
        GlobalRemapArray("udg_souls_upg_steps",
            function(i) return pState[i - 1].data.longevity or 0 end,
            function(i, value) pState[i - 1].data.longevity = value end
        )
        GlobalRemapArray("udg_souls_upg_mercDamage",
            function(i) return pState[i - 1].data.merc_dmg or 0 end,
            function(i, value) pState[i - 1].data.merc_dmg = value end
        )
        GlobalRemapArray("udg_souls_upg_mercHP",
            function(i) return pState[i - 1].data.merc_hp or 0 end,
            function(i, value) pState[i - 1].data.merc_hp = value end
        )
        GlobalRemapArray("udg_souls_upg_heroes",
            function(i) return pState[i - 1].data.souls or 0 end,
            function(i, value) pState[i - 1].data.souls = value end
        )
    end
end)
if Debug then Debug.endFile() end
 
Level 8
Joined
Feb 20, 2015
Messages
58
Version 0.8.0

Major update! Reputation system and recruiting heroes.
The team now gains reputation when they complete tasks from the tavern.
For completing regular quests, the team gains 1 reputation. When completing difficult tasks, such as destroying the orc base and the lair of the forgotten, the team receives 2-3 reputation each.
In exchange for reputation, players can recruit additional heroes in the Hero Guild.
New location added! Guild of Heroes. Here the team can recruit new heroes in exchange for reputation and gold. Hiring each next hero will be more and more expensive.

Developer comment: We wanted to increase the attractiveness of locations that are currently not in high demand: the tavern, the orc camp, the lair of the forgotten. It seems to us that we succeeded.

Other updates:
  • The "little treasure" location will now contain a random artifact of low or medium efficiency. Gold also remains on the location as a nice bonus.
  • The 26th hero, Garithos, has been added to the hero pool. He was added from the pool of standard warcraft heroes.
  • At the request of the players - the ultimate ability has been added to the "murloc" hero instead of summoning crabs. (by the way, this is not a custom hero, the murloc participated in the prologue of the orcs on the side of the enemies).
  • Fixed a bug due to which one mercenary from a previous playthrough could appear in a new run.
 
Level 8
Joined
Feb 20, 2015
Messages
58
An UNSTABLE VERSION has been released in the roguelike map.

Of the innovations: automatic saving and loading of data when playing in multiplayer. You do not need to enter anything, just play with friends and progress by the amount of wood and the development of the soul will be automatically saved.

BUT, unfortunately, this version of the map is unstable.
The instability is that there is an incompatibility between some players, due to which a synchronization error occurs at the beginning of the game.
We still cannot overcome this phenomenon.
But for some players, when playing among themselves, such problems never arise.

Since many people asked for a version with preservation - we post it here, but on the main site we will leave the old and stable version.

So, if you want to play the version with saves - play version 9.1

If you want to play the stable version, but without saves, play version 0.8
 

Attachments

  • Roguelike_v9_1 UNSTABLE.w3x
    403.9 KB · Views: 31
Level 5
Joined
Dec 27, 2020
Messages
25
I really like the map, but i think that the difficulty spike between acts is way to massive. 10 times stronger is an insanely big jump, and the game becomes impossible when reaching act 3 (since mobs deal x100 damage and take 1% damage). Upgraded skills also suck bigtime, their mana cost goes up way too much and the damage doesnt keep up with how powerful the mobs get. Also, it s really confusing that the mobs have the same stats and deal tons of damage while taking none, seeing the number would be better i believe. Exponential difficulty adjustments are very hard to balance, but i figure that mobs being 3 or 5 times stronger between acts would be a bit more balanced (upon reaching act 3 a 50 level hero cant even damage monsters and it takes multiple hits with whosyourdaddy active to kill them)
 
Level 8
Joined
Feb 20, 2015
Messages
58
I really like the map, but i think that the difficulty spike between acts is way to massive. 10 times stronger is an insanely big jump, and the game becomes impossible when reaching act 3 (since mobs deal x100 damage and take 1% damage). Upgraded skills also suck bigtime, their mana cost goes up way too much and the damage doesnt keep up with how powerful the mobs get. Also, it s really confusing that the mobs have the same stats and deal tons of damage while taking none, seeing the number would be better i believe. Exponential difficulty adjustments are very hard to balance, but i figure that mobs being 3 or 5 times stronger between acts would be a bit more balanced (upon reaching act 3 a 50 level hero cant even damage monsters and it takes multiple hits with whosyourdaddy active to kill them)

Thanks for your feedback!This really helps me a lot in terms of motivation.Now I'm developing the second version of the map, which has little in common with the first.I'm not sure if it's worth spending time polishing the current map. Or is it better to focus on the second version.In any case, thanks again for the motivation! It's important for me.
 
Level 5
Joined
Dec 27, 2020
Messages
25
Thanks for your feedback!This really helps me a lot in terms of motivation.Now I'm developing the second version of the map, which has little in common with the first.I'm not sure if it's worth spending time polishing the current map. Or is it better to focus on the second version.In any case, thanks again for the motivation! It's important for me.
Your're welcome. Good luck working on the second version, I can't wait to play it !
 
Level 8
Joined
Feb 20, 2015
Messages
58
Work on the second map based on this concept is still underway.
However, I felt that it would be irresponsible if I did not make small updates to the current version of the map. It would be irresponsible to those who enjoy playing the current version, for which I am grateful.

Based on player feedback, the following changes have been made to the map that require little effort but are desirable for players:
  • now in each subsequent chapter the parameters of mobs increase by 4 times, and not by 10 times. This is still a temporary measure, since according to the plan, the next acts should be conceptually different. But I would like to make this temporary measure more comfortable.
  • the card balance is updated according to the latest patches from Blizzards. This includes, for example, the ability to transfer mana from the Blood Sorcerer to allied units.
  • some bugs and leaks have been fixed.

To avoid problems in multiplayer, it is recommended to play in the old graphics.

Sorry for the slow process. Thanks for playing -)
 
Level 8
Joined
Feb 20, 2015
Messages
58
Update 10.1
изображение_2023-11-05_213916929.png

  • A new delicious location has appeared - the Guild of Astrologers.
  • Each visit to this guild will add a new modification for the game world to the existing ones. Thus, in one run the team can activate many modifications.

Other:
  • New world modifications have also been added to the game. At the moment, their total number is 14 pieces.
  • Due to errors, upgrades of master skills for alchemist and mechanic have been temporarily removed from the game.
  • The Tower of Challenges has become invulnerable to enemy magical attacks. Now there will be no situations where mobs destroy the AoE tower with spell damage.

More information about game modifications:
1) Age of knowledge
Find 2 altars of knowledge instead of one
2) Binge drinking
In any location there will be a bonus tavern with a 20% chance
3) Channel
Whenever someone summons a minion, 25% of the time it will summon a champion version of it with 50% bonuses to damage and health.
4) Headhunting
Get 2 reputation for destroying Orc bases. For the destruction of each tower you are given 50 coins.
5) The Restless
Approximately 30% of enemies have the ability to be Reincarnation. +25% to the wood received for clearing the area.
6) The Witcher
Killing mini-bosses gives +2 steps to old age and 5 random stat books
7) Clones
Approximately 30% of enemies have the ability of simple cloning and an additional 200 mana for units. +25% to the wood received for clearing the area.
8) Mentoring
When moving to a new location, each player’s hero receives 7 experience for each troop limit occupied
9) Target
Upon completion of the act, receive +5 to steps to old age and +250 to wood
10) Era of power
When using a spell, there is a 20% chance that the spent mana will be restored and the recovery cooldown will be reset. It works on both friends and strangers.
11) Resilient souls
85% chance for heroes, and 70% for other units that they will not die when receiving a lethal attack
12) Vengeful souls
Any unit deals up to 35% more damage depending on its lack of health.
13) Soul absorption
After death, enemies leave behind a magic rune in 15% of cases.
14) Penetrating Magic
All enemies and mercenaries lose invulnerability to magic. Instead, they gain 75% magic resistance.
 
Last edited:
Level 13
Joined
Jul 19, 2011
Messages
635
Pretty cool map
Edit: I really enjoy roguelikes in general and this hit the itch perfectly, it's 2 of the things I like a lot, WC3 and roguelikes in one!
 
Last edited:
Level 8
Joined
Feb 20, 2015
Messages
58
Update 10.5
1700949248796.png

  • Work has begun on customizing the Guardian of the Grove.
    According to the game design, the Guardian of the Grove will become a macro-hero, focused on growing a forest from summoned Ents.For a certain fee, summoned Ents can develop into battle tree guards. In addition to conducting combat operations, battle trees are capable of making improvements and hiring archers.
  • Ancient dragons (level 10) will no longer damage allies with splash attacks. The effectiveness of splash attacks on enemies has been reduced.
  • Ancient dragons (level 10) will no longer cause damage to challenge towers with their splash attacks.
  • A free Pandora's Box awaits players at the starting location.
  • The timer for initial hero selection has been increased. This is due to the fact that players were much more likely to encounter a situation where they did not have time to come to an agreement and choose a hero, rather than suffering from the player’s waiting for afk.
  • The Mentoring mod now gives not 7 but 15 experience points to heroes for each unit of limit when moving to a location.
  • Dalaran mutant is now considered undead
  • A new location has been added - Crypt, where players can purchase undead troops and undead artifacts
  • Now, at the start of a race, if there is at least one undead hero among the selected heroes, a Crypt for hiring undead will be located in the starting location
 
Last edited:
Level 8
Joined
Feb 20, 2015
Messages
58

Update 12.1

1) For each hero, starting troops and items have been added depending on the type of hero.
2) Added 13 items with unique mechanics.
3) Now the arrows of the dark hunter summon skeletons from mobs of any level. Now, for ascension of the arrow ability, the number of skeletons raised increases.
4) The "devour" ability has been removed from all mobs.
5) Some minor bugs have been fixed.

For each hero, starting troops and items have been added depending on the type of hero:​

Archmage:
  • (unit) 2x Footman
  • (unit) 1x Sorceress
  • (item) 1x Boots of Speed
Paladin:
  • (unit) 1x Knight
  • (unit) 1x Priest
  • (item) 1x Aristocrat's Document - When moving to a location, the Hero receives 40 gold coins for every 1 reputation.
Mountain King:
  • (unit) 2x Rifleman
  • (item) 1x Bottomless Health Potion
Blood Mage:
  • (unit) 2x Spellbreaker
  • (item) 1x Orb of Fire
Blademaster:
  • (unit) 1x Grunt
  • (unit) 1x Headhunter
  • (item) 2x Potion of Invulnerability
Tauren Chieftain:
  • (unit) 1x Tauren
  • (item) 1x Pendant of Energy
Far Seer:
  • (unit) 2x Raider
  • (item) 1x Mana Stone
Shadow Hunter:
  • (unit) 1x Kodo Beast
  • (item) 1x Orb of Lightning
Death Knight:
  • (unit) 1x Crypt Fiend
  • (item) 1x Bottomless Anti-Magic Elixir
Dreadlord:
  • (unit) 1x Abomination
  • (item) 1x Neverending Book of the Dead
Crypt Lord:
  • (unit) 2x Ghoul
  • (item) 1x Boots of Speed
  • (item) 1x Pendant of Energy
Lich:
  • (unit) 2x Necromancer
  • (item) 1x Soul Devourer - Each time an enemy hero is destroyed, the owner of this artifact receives 12 to mana reserve and 20 to health reserve.
Keeper of the Grove:
  • (unit) 1x Archer
  • (item) 1x Endless Wand of Winds
  • (item) 1x Pendant of Mana
Priestess of the Moon:
  • (unit) 2x Huntress
  • (item) 1x Amulet of Spell Shield
Warden:
  • (unit) 2x Dryad
  • (item) 1x Neverending Scroll of Rebirth
Demon Hunter:
  • (unit) 1x Druid of the Claw
  • (item) 1x Gloves of Haste
  • (item) 1x Shadow Orb +10
Lord Garithos:
  • (unit) 2x Knight
  • (item) 1x Never-ending Speed Rune
Murloc Sorcerer:
  • (unit) 1x Naga Royal Guard
  • (item) 1x Orb of Fire
Dark Ranger:
  • (unit) 1x Banshee
  • (unit) 1x Ghoul
  • (item) 2x Goblin Land Mines (3 charges)
Pit Lord:
  • (unit) 1x Obsidian Statue
  • (item) 1x Health Stone
Tinker:
  • (unit) 2x Wind Rider
  • (item) 1x Endless Speed Potion
Sea Witch:
  • (unit) 1x Naga Siren
  • (unit) 1x Dragon Turtle
  • (item) 1x Prophet Skull - Creates 1 random book or each altar of knowledge found.
Alchemist:
  • (unit) 1x Shaman
  • (unit) 1x Grunt
  • (item) 1x Metal detector - Find twice as many little treasures.
Firelord:
  • (unit) 1x Salamander
  • (item) 1x Dark Heart of God - For destroying an enemy base, the heart generates a book of agility +2
Brewmaster:
  • (unit) 1x Polar Furbolg Shaman
  • (item) 1x Health Stone
  • (item) 1x Mana Stone
Beastmaster:
  • (unit) 2x Druid of the Talon
  • (item) 1x Regenerating Primordial Hydra Meat (5 charges) - Use to add 1 to the food limit.
 
Level 12
Joined
Jan 4, 2014
Messages
548
Hey played this game for an hour or so and I was really blown away how well implemented the roguelike features were. Everything is so clear an intuitive, yet contains all the hallmarks and decision making aspects of a roguelike. My friends and I would discuss which path is optimal and what we were lacking. The unique options like the astrology guilds and the tower of trials were also really cool!

I think as you continue to work on this game it can really end up being one of the bets games out there.
My main issue is that the gameplay is a little dull, due to a few reasons. Here is some feedback I think could greatly improve the experience.

- Have more custom heroes. The default wc3 heroes are pretty boring for games like this. Things like getting lvl 6 tornado or being the priestess of the moon and just essentially being a starfall bot. Also consider that the blademaster and the demon hunter far outclass all the other heroes in games like this. We rolled 3x blademaster on our second run and there was no run were we ever came as close to being that strong.

- Going into unit heavy builds seems way weaker than going hero focused, even if you are willing to spend the gold needed to get a big army, they just get instantly mashed. Maybe remove the food limit or something, because it's almost never worth spending gold on that

- Some locations lack meaningful rewards. The newcomer camp and any other merc camp becomes pretty irrelevant almost immediately, whereas the tower of trials is almost always the best choice as it provides a ton of exp and items.

- The late game difficulty, before moving on the next act seems less about challenging fights and more about avoiding creeps with stuns.

Anyway I think this game has an incredible implementation of roguelike aspects but it's just missing some more gameplay features to really make it hit the spot!
 
Level 8
Joined
Feb 20, 2015
Messages
58

Divine Roguelike v13.5​

1703972612128.png

Many aspects have changed in this update.
I'm not a big fan of major updates with numerous changes, since in this situation it is difficult to separate successful decisions from unsuccessful ones.
Therefore, I will be grateful for help in the form of feedback: what I liked and what I didn’t like. How does the current version compare to the previous ones?
Main changes in version 13:
• Select the difficulty level from easy to hell. The difficulty level affects the number of monsters, their variety, the reward received and other game aspects.
• Completely changed enemy spawn system. Smoother difficulty curve. The difference in power between locations of the same level will not be as significant as in previous versions.
• The foundation for the game's plot has been laid. New narrative design tools will allow you to quickly add story dialogues in future updates.
• Implemented a mega-epic ending for the first story. It's a game within a game. Fans of hero defense should like the defense of the infernal forge.
• Rebranding has been carried out. The map changed its name to “Divine Roguelike” (may the critics forgive me).
• Some of the code has been refactored, which will facilitate faster development of future updates.

Since not as many plot texts have been added to the game as I expected to do before the New Year, I will post a summary of the first chapter below in spoilers. Happy reading.

Predictor-corrector​

This world is young and the Gods watch over it. Should its inhabitants be led astray, the Gods will interfere. They send their possessed Heroes to guide mankind through the divine plan.
People worship the Heroes not daring to go against the Gods, although they fear them.
For the legends of old are not forgotten. Chronicles tell of entire civilizations brought down by the messengers of the Gods.
And yet there are those who dare challenge the Gods. People dub them apostates and try to vanquish them before mankind has to suffer the consequences of divine retribution.

The Gods have revealed their Will​

The Final Target: find and destroy the Infernal Forge.
Hurry up! You have 14 trajections left.

Some time ago the Renegades amongst human alchemists and orc demonologists merged together to achieve common God-awful goals.
The united knowledge of this unnatural alliance has given birth to terrifying monsters and, what is more important, has called into existence a Forge with which they hope to create weapons of colossal power.
We must raze the Infernal Forge to the ground before the Renegades produce weapons capable of destroying the World.

City search​

The task: to find the City and pay a visit to it.

We wake up in a sort of camp. Unfortunately, a memory loss turned out to be a side effect of communicating with the Gods. Everything has vanished from it, except our skills and abilities. We only know the Will of the Gods - to destroy the Infernal Forge. But how to find it is obscure.
A guide called Virgil has met us at the camp and opened how to restore the memory. For this purpose, we have to visit the City and resort to the Healer of souls. That's what we're going to do.

The fragments of the broken conscious​

The task: to find 5 memory fragments taken from the Orc heroes.

The Healer of Souls related us that our memories had mixed with Divine gifts and had split off from our weak consciousness. Our memory doesn't matter. Most likely, it had been destroyed as unnecessary beyond retrieval. The Healer helped us to awaken the gift left by the Gods - to absorb the memories of defeated Orcs.
These creatures are quite easy to find at their military bases. The bigger the Orc base, the more enemy heroes inhabit it.

After absorbing enough memories of those, we will be able to determine the location of the Infernal Forge.

In the Evil Core​

The task is to raise the World danger level up to 16 and find the Infernal Forge in this very location.

We have already destroyed the required number of Orc heroes and absorbed their memories. And even they don't know the exact location of the Infernal Forge.

But here's the truth we managed to find out - due to the specific character of Forge construction, its location had become the center of concentration of monsters and creatures which had been generated by the miscreate rituals committed by alchemists and demonologists who created the Forge.

If we move towards the increasing threat, where the monsters appear most often, we will soon reach the epicenter of enemy power, where the Infernal Forge should be located.

The Infernal Challenge​

The task is to activate the Infernal Forge and destroy the waves of its defenders.

We have found the Infernal Forge.

The orcs’ memories depict that as soon as we touch the Forge, the waves of demons enslaved by alchemists will incarnate next to it in order to destroy us and protect the Forge. We should get ready.


And if there's still some time left, it might be better to return to the Forge later, when we get stronger. But if time is over, then there is nothing left but to try to destroy the defenders of the Forge right now.

(if the player wins) The forge has been liquidated​

The task is to improve your souls, possess the rebirth skill, rise in new heroes and choose a new mission.

The Infernal Forge was destroyed. Your mission is accomplished. Our bodies have weakened and lost their former power. The Gods have destroyed them as unnecessary.

Our souls wake up in the Sacrarium. We now here in the sacred place have to teach our souls the new skills in order that the next Heroes, whose bodies we inhabit next time, could inherit the accumulated experience.

Then we will be reborn to fulfill the new orders of the Gods.

(if the player loses) This is a dream, not a defeat​

The task is to improve your souls, possess the rebirth skill, rise in new heroes and try again.
We were not able to finish our mission in time.
The Infernal Forge has produced the weapons of power never yet seen, which plunged the world into chaos.
But how could the Gods have made a mistake in choosing Heroes?
They were not mistaken. After the defeat, the souls of the heroes wake up in the Sacrarium.

This campaign, this failed mission - it was not real, it was a dream created by Morpheus. In those dreams, the souls experience the same events over and over again until they are ready to fulfill their purpose.

Decades in the Morpheus’ Kingdom for the deceased souls is one day in the real World. The gods are bringing along the souls looking for the suitable bodies for them - the true Champions among Heroes. In the Sacrarium we can transform the experience gained by souls in Morpheus' dreams into the practical skills of future Heroes.
 
Level 6
Joined
May 5, 2021
Messages
28
Yo yo
I played this map quite a bit in singleplayer lately and i am having a ton of fun. Sadly i have to agree with @Eat_Bacon_Daily that unit builds are not strong enough right now. I really want to make Keeper work but the soul investment needed to make units stronger is so huge. Demon hunter seems pretty OP as the cost of having immolation running is almost (maybe not at all?) increasing with upgrades which paired with the buff that breaks magic resistances and his amazing base stats makes him much better than any other hero I tried.

That being said I really enjoy the map as it does the random "dungeon" generation so well were you always want to think about the next move and with the random buffs and increase in soulpower replayability is through the roof.

To make it even better it would be cool with some more customized heroes and the opportunity to upgrade all of your spells instead of just one or two for each hero. Also better hero balancing so you don't reroll the run just because there were no "good" hero options

Of course more story will also be great but it is very clear that you are going to add that so i wanted to comment more on other things.

Keep up the good work!
 
Level 3
Joined
Jul 12, 2006
Messages
23
Been playing this in single player a good amount as well. The game is really good and mainly needs balancing. I agree with most of what people have been saying above with some additions

  • Scepter of healing far outclasses any other items outside maybe killmam (spelling? the lifesteal one) and actually allows unit builds to work. Although heroes also just become even stronger.
  • I wish there was a way to get more race units than the starting ones
  • Said before but I dislike newbie camps reoccurring
  • The 70% chance to not die is easily the nastiest condition out of them all, a must avoid for me, and it doesn't even give wood
  • Dragons/Hydra good still
  • I like the ability boosting system but a lot just reduce your effective dps for a bigger burst. This is really only feasible to me on int based heroes since the cost rises so dramatically that it is never worth it. Also Illidan
  • The 2 reputation per orc camp talent, is OP. If you can get 3 heroes early enough you can easily win and this is really the only reliable way without 50+ steps

Fun game though, good job
 
Level 8
Joined
Feb 20, 2015
Messages
58
Played it for myself, I like that you can continue afterward with saved progress.
For myself, I think that map should have more custom spells or revamped abilities for heroes, cause I see that the core idea is in a very good state.

Anyway, I played it again and enjoyed it.
Thanks for the review and the game! Later I will add a video to the zero post -)
 
Top