Log In  

Hello, considering the following:

Trying to remake Vampire Survivors and a fun project
I have a character class
Characters have effects that are applied to a target. In this example my target = player
I have a character that is an instance of that class. Let's call him "Antonio"
I have a "run", Antonio is copied into run.player
I set player to be run.player

My problem is that I have to initiate the character class and Antonio before I create "player". But player is not yet created so player referenced on the character class is nil.

I hope this makes sense, I have a workaround but it feels clunky and I'm wondering if there are other solutions.

Here is my code before the workaround:

character = {}
character.__index = character
function character:new(name)
    local o = setmetatable({}, character)
    o.name = name
    o.x = 0
    o.y = 0
    o.sp_scale = 1
    o.w = 16
    o.h = 16
    o.effects = effects
    o.original_invincibility_frames = 10
    o.invincibility_frames = 0
    o.max_health = 100
    o.health = 20
    o.recovery = 0
    o.armor = 0
    o.move_speed = 1
    o.might = 1
    o.weapon_projectile_speed = 0
    o.weapon_duration = 0
    o.weapon_area = 0
    o.weapon_cooldown = 0
    o.weapon_projectiles_amount = 0
    o.revival = 0
    o.magnet = 0
    o.luck = 0
    o.growth = 0
    o.greed = 0
    o.curse = 0
    o.reroll = 0
    o.skip = 0
    o.banish = 0
    return o
end

function character:set_effects(effects)
    self.effects = effects
end

function character:apply_effects(level)

    local effects = self.effects
    for _, effect in pairs(effects) do
        if level % effect.level_increment == 0 and 
            level <= effect.max_level_increment then 
            local target = effect.target    
            if effect.mode == "percent" then
                target[effect.stat] += (target[effect.stat] or 0) * effect.value
            else
                target[effect.stat] = (target[effect.stat] or 0) + effect.value
            end
        end

    end
end

antonio = character:new("antonio")
antonio.recovery = 0.5
antonio:set_effects( {

                    {   description="Gains 10% Might every 2 Levels",  
                        stat = "might", 
                        value = 1,
                        target = player, -- here is the problem, player doesn't exist
                        level_increment = 2,
                        max_level_increment = 10,
                        mode = "percent"
                    },
                    {   description="Gains 0.5 Recovery every 1 level", 

                        stat = "recovery", 
                        value = 0.5,
                        target = player,
                        level_increment = 1,
                        max_level_increment = 5
                    }

                }

)

function _init()
    t = 0
    run = {}
    run.player = {}
    run.level = 1
    add(run.player, deepcopy(antonio))

    player = run.player[1]

end

function _update()

    if t % 60 == 0 then player.health += player.recovery end 
    if btnp(4) then
        run.level += 1
        player:apply_effects(run.level)
    end
    t += 1
end

function _draw()
    local y = 0
    cls(0)
    for entry in all(run.player) do
        for k, v in pairs(entry) do
            if type(v) == "number" then
                print(k..":"..v, 0, y)
                y += 8
            end
        end
    end
    local y = 0
    print("Level: "..run.level, 240, y)

end

function deepcopy(orig)
    local orig_type = type(orig)
    local copy
    if orig_type == 'table' then
        copy = {}
        for orig_key, orig_value in next, orig, nil do
            copy[deepcopy(orig_key)] = deepcopy(orig_value)
        end
        setmetatable(copy, getmetatable(orig))
    else -- for numbers, strings, booleans, etc
        copy = orig
    end
    return copy

end

And here is my code with the workaround, in short, once player is created I go and apply player as the target of the effects: player:update_effects_target(player)

character = {}
character.__index = character
function character:new(name)
    local o = setmetatable({}, character)
    o.name = name
    o.x = 0
    o.y = 0
    o.sp_scale = 1
    o.w = 16
    o.h = 16
    o.effects = effects
    o.original_invincibility_frames = 10
    o.invincibility_frames = 0
    o.max_health = 100
    o.health = 20
    o.recovery = 0
    o.armor = 0
    o.move_speed = 1
    o.might = 1
    o.weapon_projectile_speed = 0
    o.weapon_duration = 0
    o.weapon_area = 0
    o.weapon_cooldown = 0
    o.weapon_projectiles_amount = 0
    o.revival = 0
    o.magnet = 0
    o.luck = 0
    o.growth = 0
    o.greed = 0
    o.curse = 0
    o.reroll = 0
    o.skip = 0
    o.banish = 0
    return o
end

function character:set_effects(effects)
    self.effects = effects
end

function character:update_effects_target(target)
    for _, effect in pairs(self.effects) do
        effect.target = target
    end
end

function character:apply_effects(level)

    local effects = self.effects
    for _, effect in pairs(effects) do
        if level % effect.level_increment == 0 and 
            level <= effect.max_level_increment then 
            local target = effect.target    
            if effect.mode == "percent" then
                target[effect.stat] += (target[effect.stat] or 0) * effect.value
            else
                target[effect.stat] = (target[effect.stat] or 0) + effect.value
            end
        end

    end
end

antonio = character:new("antonio")
antonio.recovery = 0.5
antonio:set_effects( {

                    {   description="Gains 10% Might every 2 Levels",  
                        stat = "might", 
                        value = 1,
                        target = player, -- here is the problem, player doesn't exist
                        level_increment = 2,
                        max_level_increment = 10,
                        mode = "percent"
                    },
                    {   description="Gains 0.5 Recovery every 1 level", 

                        stat = "recovery", 
                        value = 0.5,
                        target = player,
                        level_increment = 1,
                        max_level_increment = 5
                    }

                }

)

function _init()
    t = 0
    run = {}
    run.player = {}
    run.level = 1
    add(run.player, deepcopy(antonio))

    player = run.player[1]
    player:update_effects_target(player)
end

function _update()

    if t % 60 == 0 then player.health += player.recovery end 
    if btnp(4) then
        run.level += 1
        player:apply_effects(run.level)
    end
    t += 1
end

function _draw()
    local y = 0
    cls(0)
    for entry in all(run.player) do
        for k, v in pairs(entry) do
            if type(v) == "number" then
                print(k..":"..v, 0, y)
                y += 8
            end
        end
    end
    local y = 0
    print("Level: "..run.level, 240, y)

end

function deepcopy(orig)
    local orig_type = type(orig)
    local copy
    if orig_type == 'table' then
        copy = {}
        for orig_key, orig_value in next, orig, nil do
            copy[deepcopy(orig_key)] = deepcopy(orig_value)
        end
        setmetatable(copy, getmetatable(orig))
    else -- for numbers, strings, booleans, etc
        copy = orig
    end
    return copy

end
P#147242 2024-04-24 10:32 ( Edited 2024-04-24 10:32)

There's honestly nothing wrong with your solution. In fact, I'll go so far as to say it's a pretty good solution. If gives you additional flexibility if, sometime in the future, you want to be able to swap targets during play for whatever reason. I only have a vague idea of how Vampire Survivors works so this may not be relevant but I'd actually expand update_effects_target slightly so you can change the target of specific effects instead of having to change all of them.

That said, if you're really unsatisfied with it there are other solutions.

The simplest is to just change where you're calling set_effects. You don't have to call it on antonio but instead just wait and call it on player at the end of _init. That will work but it's not super flexible and I'm guessing you'll want more flexibility.

The other option is to defer setting the target until it's actually needed. Basically instead of setting the target to the actual target object itself, set it to a function which will return the target object and then call that function when applying effects.

Something like this:

-- player doesn't exist yet but it doesn't need to. It only needs to
-- exist by the time this function is called.
function get_player()
   return player
end

-- definition of character class and set_effect unchanged

function character:apply_effects(level)

    local effects = self.effects
    for _, effect in pairs(effects) do
        if level % effect.level_increment == 0 and 
            level <= effect.max_level_increment then 
            local target = effect.target    

            -- This is the only part that's different. If target is a
            -- function then call that function to get the actual
            -- target.
            if type(target) == 'function' then
               target = target()
            end

            if effect.mode == "percent" then
                target[effect.stat] += (target[effect.stat] or 0) * effect.value
            else
                target[effect.stat] = (target[effect.stat] or 0) + effect.value
            end
        end

    end
end

antonio = character:new("antonio")
antonio.recovery = 0.5
antonio:set_effects( {

                    {   description="Gains 10% Might every 2 Levels",  
                        stat = "might", 
                        value = 1,

                        -- player doesn't exist but the get_player function does.
                        -- By the time the function is called player will exist.
                        target = get_player, 

                        level_increment = 2,
                        max_level_increment = 10,
                        mode = "percent"
                    },
                    {   description="Gains 0.5 Recovery every 1 level", 

                        stat = "recovery", 
                        value = 0.5,
                        target = get_player,
                        level_increment = 1,
                        max_level_increment = 5
                    }

                }
)

-- Everything else stays the same.
P#147247 2024-04-24 11:43

Thank you for the thoughts, appreciate it.

My original thinking was that "seems inelegant = wrong solution" so it's good to know I'm not completely crazy. I will try out your other ideas.

P#147260 2024-04-24 15:34

[Please log in to post a comment]