Implement Statuses
Closed this issue · 1 comments
Statuses
First, by status, I mean supportive or destructive effects on an entity, like buffs, stun, frozen, poison etc.
They should be able to
- run some game logic in every frame (e.g. poison damages the target entity in every X seconds)
- modify the target entity's attributes (increase/decrease attack speed, attack power, moving speed, etc)
- modify the target entity's ability to move/cast skills (e.g. frozen, stun stops entities from moving)
- change the render logic of the target (e.g. for poison, the target should be green, for frozen state, an ice effect should be rendered on the sprite)
- remove existing statuses from the target entity (e.g. stun and sleep can't be on the same target at once)
Multiple statuses can have different effects on rendering the entity, e.g. one Buff can increase the size of the entity, and a Poison can make it green. In that case we have to render an increased, green sprite. If that entity then gets stunned, then we have to render the stun effect above the increased, green sprite (so the y offset of the stun effect will be different than for a normal sprite).
Immediate or retained calculation
Player attributes (atk, armor etc) are modified by statuses. The naive approach would be to have for example an atk
field on entity, then when a Buff status is applied on that entity, it would modify that atk
field, like adding +10% to it.
However it would lead to unmaintainable and complicated code.
- You would have to reset the original value after the Buff has expired, which is almost impossible. Imagine the situation where you have two buffs on your target, one which increases atk by 100, then the other by +10%. What if the first Buf expires earlier than the second one, and subtract 100 from the current
atk
(which is increased by 10%), and then the second one removes 10% from the reducedatk
. - You would not know why
atk
has a specific value. If there is a bug, and some status forgets to clean up his work, it is more difficult to find that bug. - Sometimes you need the base
atk
of a character, so you can't just modify it. - etc
So rather I choose immediate calculation, where entity attributes are calculated on demand, in every frame if necessary. It would have a base attribute data structure, which then would be copied and modified by all the active Statuses on the entity. That copied attribute structure will be used for damage calculations.
Algorithmic behaviour
My assumption is that there won't be much Status on an entity at the same time, maximum ~10. In a team fight, they change rapidly, then have a bigger time span when they don't change at all (e.g. go into a team fight will add/remove many statuses on almost all the players. After and before the fight it almost does not change at all).
My approach
"Important" and not so "important" Statuses
Many skills and effects will be implemented by statuses, which first does not seem as a Status. E.g. a wizard put a fire bomb on a character. Before expiration, it renders some fire effect on the character, then on expiration it damages the character and its area. This is a perfect example for a Status, however it is rather strongly related to the "fire bomb" skill, and I would put the code of this Status in the same file as the skill which created the status.
Since some game logic has to figure out if a specific Status is applied on an entity (e.g. some skills damages more on Stunned entities), some States have to have an identifier, a "type". But I would not give an own enum for every "not so important" status, since they won't be queried for and other parts of the code does not have to know about them.
So to avoid polluting the code with those Skill-related statuses, I distinguish "important" and "not so important" Statuses.
Important Statuses are the ones who are known globally, does not relate to one concrete skill, and used heavily by other codes, statuses, calculations etc. For example:
- Stun
- Frozen
- Rooted
- Sleep
- etc
Design
The enum will look something like this:
pub enum StatusType {
Stun(ElapsedTime),
Frozen(ElapsedTime),
Root(ElapsedTime),
Sleep(ElapsedTime),
// "not so important" statuses
OtherHarmfulStatus(Box<impl Status>),
OtherSupportiveStatus(Box<impl Status>)
}
With this enum, it is possible to implement skills like "remove all negative status from the target".
Status
will be a trait with functions like
fn can_target_move -> bool
fn can_target_cast -> bool
(e.g. being rooted disallows moving but allows casting skills)fn get_render_effect(&self) -> ???
(It might be necessary to render more than one effect on the target, so the return type is in question)fn get_render_color -> [f32; 4]
fn get_render_size -> f32
fn calc_attribs(&self, &mut CharAttribs)
fn update(&mut self, now: ElapsedTime)
fn get_duration_percent_for_rendering() -> Option<f32>
: This is for rendering a decreasing status bar above the character which tells when the next status will expire
Entities will have a Vec<Status>
field.
Then:
- Statuses go into the
Vec
in insertion order - Many parts of the code will iterate through the
Vec
and call those functions to determine attributes and rendering properties of the target entity - They will remove themselves when it is necessary
- Querying an entity for a specific status requires linear searching
To avoid elements to be moved when Statuses are added to the Vec
, I might use an [Option<Status>; 32]
instead.
Implement these to test the design:
- Buff + Poison + Stun
- Create a buff skill which increase the size of the target
- Create a poison status. It damages the player in every second, and makes it green.
- Implement the stun status. The stun effect must appear above the increased size sprite.
- Buff + Poison + Frozen
- Same as above, but the Frozen effect sprite must also be increased
- Fire Bomb
- A skill which can be casted on an enemy.
- After x seconds, it explodes and damages the target and its area
- A "decreasing casting bar" should appear above the character to show when the status will expire
- Frozen state should be broken when the target is attacked.
'Mounted' is also a Status.
I might drop the enum Status
idea and will use fixed indices for important Statuses in the array, e.g.
Stun is at 0, Mounted is at 1 and so on.
pub fn is_stunned(&self) -> bool { self.statuses[STUN_INDEX].is_some() }