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 |
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. |
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.
[Please log in to post a comment]