ID:1806324
 
(See the best response by Ter13.)
So I have an object that when clicked calls a certain function. My goal is for my function to be usable five times by the user before it goes on delay. My idea was to make a while loop and set an integer usableTimes to 5 and then set my while loop up with usableTimes. Now the problem I'm having is in my while loop how can I use click to refer to the function rather than an object. If I'm to refer to an object and it's object which called the function it would require another click and that would mean a double click for each decrement but that's not what I want. I'm having trouble explaining but the main goal is object A when clicked calls Function A ; the first line of function A sets a new integer usableTimes to 5 and then there's a while loop to check each time Something sets off function A to activate it and decrement usableTimes by 1(The something sets off being Click I suppose). I'm not sure i've thought of several solutions but I can't seem to apply click correctly. Thank you.
Best response
What you are describing is a buff/debuff tracker. Buff/debuff trackers got their start in RPGs as temporary status effects. Usually, temporary status effects were hard-coded individually for RPGs. Soon, they made their way into other genres, but MMOs took the concept further by making a generalized system for handling buff/debuffs. Most notably, a variant of buff/debuff tracking was written for World of Warcraft. They called them "auras".

The basic idea of a buff/debuff tracker is that you need three things to make them work properly: You need a unique ID to refer to the buff/debuff by. You need a behavior that the buff/debuff will impose, and you need a timer that will manage the buff/debuff stack.

I wrote a very open-ended and general (but powerful) debuff/buff tracker for SDBO recently, but I'm not in the mood to share it even though I feel that it's pretty close to perfect for any possible use-case.

Instead, I'll show you a very basic implementation of a specific debuff tracker rather than a general one.

mob
var
list/buffs
proc
AddBuff(id,buff)
if(!buffs)
buffs = list()
if(buffs[id]==null)
buffs[id] = buff
buff.onAdded(src)
return buff
else
return buffs[id]

RemoveBuff(id,buff=null)
if(buffs)
var/buff/b = buffs[id]
if(buff==null || buff && b==buff)
buffs.Remove(id)
else if(buff==null)
buffs.Remove(id)
else
return null
if(b)
b.onRemoved()
if(buffs.len==0)
buffs = null
return b
return null

getBuff(id)
if(buffs)
return buffs[id]
return null

//if you ever need to delete a mob, run Cleanup to clear circular references and then put the mob's location at null. If you have anything else that references this mob, it won't be garbage collected until those references are clear. Best to keep track of these via circular references.
Cleanup()
var/buff/b
for(var/v in buffs)
b = buffs[v]
b.owner = null
buffs = null
src.loc = null

Read(var/savefile/F)
..()
var/buff/b
for(var/v in buffs)
b = buffs[v]
b.owner = src

buff
var
id
tmp
mob/owner
expire_time
proc
setExpireTime(duration)
if(duration>0)
expire_time = world.time + duration
var/oldtime = expire_time
spawn(oldtime-world.time)
if(owner&&oldtime==expire_time)
onExpire()

onRemoved()
owner = null

onAdded(mob/m)
owner = m

onExpire()
owner.RemoveBuff(src.id,src)

New(Id,Duration)
id = Id
setExpireTime(Duration)

Read(var/savefile/F)
..()
F["expire_time"] >> expire_time
setExpireTime(expire_time)

Write(var/savefile/F)
..()
var/expiry = expire_time-world.time
F["expire_time"] << expiry


Okay, what this system basically does, is create a datum structure you can use to create timed buffs/debuffs. You can extend these buff objects and give them new behavior by overriding onRemoved() and onAdded().
To follow on with my approach, let's take a look at how it can be used to solve your specific problem.

We need to create a stacking buff. Let's do that real fast. This will allow us to keep track of how many times a buff is stacked up. When you stack a buff, it will reset the unstack timer. The unstack timer will determine how many ticks we will wait before decreasing the stack level. If the stack level reaches zero, we remove the buff. If the stack level reaches max stack level, we simply reset the unstack timer, but don't add any more stacks.

buff
stacking
var
stack = 1
max_stack = 5
stack_timer = 10 //lose 1 stack every 10 seconds.
proc
onStack()
setExpireTime(stack_timer)
if(stack<max_stack)
stack += 1

onUnstack()
if(stack>1)
stack -= 1
setExpireTime(stack_timer)
else
owner.RemoveBuff(id,src)

onExpire()
onUnstack()

New(Id,Duration,MaxStack)
..()
max_stack = MaxStack


Alright, let's get this show on the road. Let's see how you'd use this to make something usable on-click 5 times before triggering a long cooldown.

Click()
var/buff/b = usr.getBuff("clickcooldown")
if(b)
return //fail out, as we're on cooldown
var/buff/stacking/sb = usr.getBuff("clicktracker")
if(sb)
sb.onStack()
if(sb.stack==5)
b = new/buff("clickcooldown",100)
usr.AddBuff("clickcooldown",b)
else
sb = new/buff/stacking("clicktracker",10,5)
usr.AddBuff("clicktracker",sb)
//ADD CODE FOR ITEM USE HERE.


That's all there is to it. You've just got to initialize an instance of the two types of buffs/debuffs and add them to the player's buff tracker. From there, the buffs themselves will do the legwork.

Also, in this case, you might want the event to occur on MouseDown/MouseUp rather than on Click if you want to avoid the double-click problem.
wow thanks so much this is very detailed and helpful. also the pseudo code is fine enough haha i do appreciate your honesty though. It's more fun to see what I can build with the concept you have shown. Thanks again.
also the pseudo code is fine enough haha i do appreciate your honesty though.

Not sure I follow. This isn't pseudo-code. This is a full implementation. I haven't compiled it, but if there's any adjustment needed, it's minor. It's just more simplistic than the actual one that I use.

The one I actually use would probably not be of any help to you, because it has a lot of very advanced patterns that in order to understand, you'd have had to have run into the limitations of this approach first to even start to grasp why I do some of the things I do in my approach.
Oh I just meant it's easy to read and follow. I guess I don't know the real meaning of psuedo code. Professor's in college usually say write psuedo code for bubble sort or binary tree search for our projects and I assumed they meant write your own version that's easy to read(since they have us comment on the lines). But yeah I'd definitely not be able to follow your version because as you said advanced patterns would be hard to grasp. I'm curious though by what you said about patterns. I'm taking a cs 335 course next semester and a portion of it is focused on patterns which I'm hoping will be the class that furthers my programming skills to a next degree since so far we've focused on two semesters of java, discrete math, and C but haven't really gotten too deeply advanced into any programming language to the point of using advanced patterns. Hardest thing we did was probably the binary search tree and library double linked lists projects. Anyway thanks again I applied the logic and got it to work.
The patterns I use in my version are the observer pattern and jagged lists for subtype storage.

Basically, I want to be able to have multiple buffs with the same ID, but with a different source entity sometimes not override each other, but rather have a mutual effect. I have to use jagged lists for this if I want to be able to get a copy of all effects with the same ID, but with a different subid.

I make buffs mutually self aware of one another by having them listen for buffs being added, removed, expiring, stacking, unstacking, etc. I do this through the observer pattern.

These patterns actually bloat the code of my version of this by about 400 lines even with the most optimized version I've managed to come up with so far.
I'm not sure I'd name the buff "clicktracker" or "clickcooldown", because those names would apply to all such clickable items. It sounds like this same technique might be needed across multiple clickable items. And if that's the case, I'd go with a different approach.

If for instance each clickable object is an icon for a HUD or a grid, represented by an obj, then I'd do something like this:
datum/proc/BuffCooled(bufftimer/timer)  // generic proc when buff cools down

bufftimer
var/datum/item
var/cooldown = 600
var/timer_id = 0
var/queued = 0
var/start = 0 // start time

New(datum/_item, _cooldown)
item = _item
cooldown = _cooldown

// still cooling
proc/Cooling()
return timer_id

// returns fraction from 0 to 1
proc/Progress()
return (world.time-start) / max(1,cooldown)

// calling this again aborts the old timer, unless queue arg is true
// if queuing, will cycle again after this cycle is finished
proc/Cool(queue)
if(queue && timer_id)
++queued
return
spawn(-1)
do
start = world.time
if(queued) --queued
var/t = ++timer_id
sleep(cooldown)
if(!item || t != timer_id) return
timer_id = 0
spawn(-1) item.BuffCooled()
while(queued)

obj/skill
var/tmp/bufftimer/timer // don't save the timer
var/uses = 0
var/max_uses = 5
var/cooldown = 600 // 1 minute at 10 FPS
mouse_opacity = 2

New()
// make sure this starts cooling down again if loaded from a savefile
if(uses) Cool()
UpdateDisplay()

// In this model, cooldown period refers to time before you get 1 use back.
// Every new use initiates a new cooldown to decrement uses.
// E.g. if you don't use this skill again within the cooldown period, it gets a +1.
proc/Cool()
if(!timer) timer = new(src, cooldown)
timer.Cool()

BuffCooled(t)
if(t != timer) return // sanity check
// If you want to restore full power here, set uses = 0 instead.
uses = max(0, uses-1)
if(uses) Cool() // cycle again
else timer = null // soft-delete datum (avoids cicular reference)
UpdateDisplay()

proc/UpdateDisplay()
// Change the icon, maptext, alpha etc. here.
// This is just one possibility:
alpha = (uses < max_uses) ? 255 : 128
if(uses < max_uses && max_uses > 1)
var/obj/O = new
O.maptext = "<span valign=bottom align=right style=\"color:white\">[max_uses - uses]</span>"
O.maptext_width = 32 // assuming 32x32 icon size
O.maptext_height = 32
overlays = list(O)
else overlays = null

Click()
if(uses >= max_uses) return // spent all uses
++uses
Cool()
UpdateDisplay()
Action()

proc/Action()
// Override this proc for all skill types.
// usr is safe here because this is only called by verbs!
I'm not sure I'd name the buff "clicktracker" or "clickcooldown", because those names would apply to all such clickable items. It sounds like this same technique might be needed across multiple clickable items. And if that's the case, I'd go with a different approach.

Definitely not. The names were just for the sake of example.

Actually, our examples are entirely different concepts. Yours seems to be tied to the object itself, while mine is tied to the player themselves. What I'm demonstrating is a concept called "global cooldowns". And given the nature of my approach, I'd argue that mine can do quite a lot of things that yours can't that are common in games, while yours can do a single thing that mine can't --which is incredibly uncommon in games.

I thought I'd take a minute to outline some of these problems:

The global cooldown problem:

In MMORPGs such as WoW, there's a concept called the global cooldown. Basically, this cooldown applies to all abilities, all items, etc. Some abilities trigger the global cooldown, some don't. A global cooldown is usually very short, maybe a half of a second or so, but it prevents you from spamming 5-6 attacks that are all on different cooldowns, and it allows you to implement a one-at-a-time attack setup without having to spend the time developing the code to track when a player is channeling an attack. You just use the existing cooldown system to prevent the player from doing anything until the attack channel time is done.

The shared cooldown problem:

In MMORPGs, some attacks or items share the same cooldown. In your approach, I'm not sure that would be possible. This is to help prevent players from collecting a large number of items that deal direct damage on-use, or swapping their on-use trinkets to stack up bonuses. In my approach, you could prevent the drinking of a small health potion after drinking a large health potion simply by having small and large health potions use the same cooldown id.

Problems with my approach:

The per-instance cooldown problem:

In MMORPGS, nobody ever makes it so that an instance of an item has a separate cooldown from a second instance of the same item type. I suppose if you wanted to make this possible using my approach, you would need to set up some kind of a uniqueid system for item instances that persists between hosted sessions of the world and also avoids overflowing.

The item-swap cooldown problem:

Players can trade items with one another, and since the cooldown is associated with the player, not the item, the item will be immediately usable by the player. This means that you could potentially share trinkets between two or more characters and end up with buffs being applied to the player as though each player had their own version of the trinket. This wouldn't be possible in your approach, but it would be possible in mine. That could be a good thing, or it could be a bad thing depending on how you look at it.

This could be addressed with my approach by keeping track of the buff on the specific item itself, and when the player drops/destroys/trades the item, the buff will be removed but the cooldown will stay with the player. That'd be one way to pull it off.

Another way MMOs handle this, is making it so that any item that has a cooldown is usually soulbound and thus can't be traded.

And finally, you could also get around this problem by just keeping track of the global time of the current cooldown on the item itself, then when the item enters their inventory, apply the cooldown to the item.

Many MMOs also trigger the cooldown of an item when you first equip it without applying the buff, or when you first acquire it, making it so that you have to wait until it expires to use it as well.



Our approaches definitely do different things entirely. I don't care for tying cooldowns to the item itself mainly because cooldown behavior is something that I see being associated to the player rather than the item/skill itself. To me, "not being able to do something" is most often a player concern rather than an item concern. Then there are the design concerns I mentioned with both of our approaches. Overall, I think my approach more closely mirrors useful gameplay mechanics that have been well established in the MMO genre, while your approach solves a single limitation of my approach (one that is not normally represented in the MMO genre), while bricking off some very common mechanics.
Fascinating. That's a lot of info to digest, but a good read for anyone considering MMO mechanics.

I'm still not sold on the idea of basing the buff/cooldown on names. But broadly speaking, I think my bufftimer setup could be used exactly like you said. Nothing would prevent, for instance, a skill from also checking on or creating a timer belonging to the mob.

mob/var/tmp/list/bufftimers

// global
var/list/bufftimes = list(
"attack" = 10,
"defend" = 5,
...
)

mob/proc/BuffTime(id)
// can override this with special instructions to deviate from the global list
// but by default, use the global list
return bufftimes[id] || 600

mob/proc/BuffFor(id, create) // id could be a name or object
if(!bufftimers) bufftimers = new
. = bufftimers[id]
if(!. && create)
. = new bufftimer(src, BuffTime(id))
bufftimers[id] = .

mob/proc/BuffCooling(id)
var/bufftimer/t = BuffFor(id)
return t && t.Cooling()

mob/proc/BuffCool(id, queue)
var/bufftimer/t = BuffFor(id, 1)
if(t) t.Cool(queue)

So with only a little modification, the mob still can manage its internal buffs, and items, skills, etc. can check those buffs. I'd probably modify this system further to make it a little more sophisticated, but I think the timer is the core of it.
Wow this is all a ton of great information. Thanks guys wish I could best response all of it.