Player party
Player party¶
The player party is a BattleData array field called playerdata
. It's one of the most important field in the game because it contains overworld and battle party information that is used throughout the game in several ways. This includes stats, current battle states, and the dual entities backing it (entity
for the overworld, battleentity
for the battles).
playerdata
addressing¶
The most complex part about playerdata
is how the game addresses it. There's multiple ways to do it, each have their unique properties and they completely change depending on if we want to address the party in the overworld or in battle. There are 3 distinct ordering of the party:
trueid
order- Overworld party order
- Battle formation order
Since the addressing part is so complex, let's talk about it before going over the methods involved in managing or querying the party.
Universal playerdata
addressing¶
Here are all the ways to address playerdata
that works regardless if we're addressing in the overworld or battles:
- Regular array index: Doesn't guarantee anything except if all party member are in
trueid
order and the first one has atrueid
of 0 (Bee
) where in such case, the array indexes will always be the same as thetrueid
- Finding the member with a specific BattleData's
trueid
: The animid associated with this party member at the time of the last ChangeParty which never changes until the next ChangeParty so it doesn't take into considerations overworld switching - Finding the member with a specific
entity
orbattleentity
'sanimid
: The rendered animid of the party member taking into account all overworld switches (entity
for the overworld,battleentity
for battles)
The above remains true in all cases. While it's straight forward to understand why the entity's animid allows to address by rendered animid, what's less straight forward is why the array index doesn't guarantee anything and how the trueid
works.
Addressing by array indexes directly¶
Essentially, the player party system supports to have any combination of members in any order. The formation can even change which is done by a method called ChangeParty (more details about it in a section below). It receives an int array parameter called ids that tells what animid should be in the party and in what order. It doesn't have to include the first 3 animids, it could have any combinations of them and any amount, but the game is designed to only support animids from 0 to 2.
While for most of the game, the party is composed of Bee
, Beetle
and Moth
which happens to match with the array index (0, 1 and 2), the game allows for different party formations and to even excluse memebers. It is very well possible to have a party of Moth
and Beetle
, but exclude Bee
. It's even possible to have a party of a single member that isn't Bee
. All these cases would lead into a situation where playerdata[0]
may not actually be the Bee
member. It might be someone else entirely simply because Bee
wasn't the first party member (or they weren't present in the party at all).
Because of this, addressing playerdata
by a direct index is the least useful way to address the party. It's only used when performing operations where not knowing whose member is who is acceptable or when looping through the playerdata
array to do something on all members. It's more useful to enumerate the party in a certain way with the 3 different ordering that are detailed in the sections below.
trueid
order¶
Due to this problem, this is where the trueid
comes in. When the party gets created by ChangeParty, all playerdata
will have their trueid
set to the matching one sent in the ids parameter and it will never change until another ChangeParty.
Effectively, this allows to find out the animid each playerdata
had at the moment of the last ChangeParty. This ordering is refered to as the trueid
order and it is useful because it tells the canonical party order without considering any overworld switches. This order is also stored in partyorder
which is also set by ChangeParty. However, since it doesn't consider any overworld switches, it means it's not as informative in the overworld because it doesn't tell who each playerdata
is AT THE CURRENT MOMENT. It only tells who they were on the last ChangeParty. This method of addressing however becomes much more useful during battle which will be detailed in a section below.
Addressing playerdata
in the overworld¶
In the overworld, the party can be switched using SwitchParty(false). Doing this process renders the trueid
less useful because the order of the party since the last ChangeParty is no longer the same then the current one. Moreover, addressing by entity
.animid
isn't sufficient because this only gets updated on the next RefreshEntities at the very end of a switch animation. It would be far more useful to have a way to know who's playerdata
element is whom that follows the exact moment SwitchParty(false) is called.
Overworld party order¶
This is where the BattleData's animid
comes in. Not to be confused with the entity
.animid
(which is the actual rendered animid), the BattleData's animid
is a working value of entity
.animid
. It's basically the value entity
.animid
will be on the next RefreshEntities.
The reason this is useful is because overworld switching cycles all BattleData's animid
. Effectively, they visually cycle their animid, but they don't actually change anything else. Finding a playerdata
that has an animid
of a specific value is the most reliable to find a member by its animid because it's essentially the trueid
, but it follows overworld switches. This is refered to as the overworld party order: it allows to enumerate the party by their order in the overworld from front to back. For example, playerdata[0].animid
always gives the logical animid of the leader in the overworld.
The only downside of the BattleData's animid
is it doesn't reflect the ACTUALLY RENDERED animid. This is because like explained above, between the moment SwitchParty(false) is called and the moment the next RefreshEntities happen, there's a gap of time (15.0 frames) where the entity
.animid
won't be the same as the BattleData's animid
and the entity
.animid
will lag behind.
To address by rendered animid, finding the member with a specific entity
.animid
is required. This can be useful for performing specific animations, but it's oftentime more recommended to address by BattleData's animid
because it will always be logically accurate in at most 15.0 frames while the entity
.animid
might become outdated in at most 15.0 frames.
Addressing playerdata
in battles¶
Battles changes a lot how playerdata
is addressed because switching the party works very differently using SwitchParty(true) and the starting order of the party in playerdata
always matches the trueid
order. To understand why, let's talk about what SwitchParty(true) does differently.
Battle formation order¶
Unlike SwitchParty(false), SwitchParty(true) doesn't actually changes anything in any playerdata
. What it does instead is cycle battle.partypointer
's elements which was first assigned during StartBattle. battle.partypointer
is a mapper that maps logical battle formation position to playerdata
indexes. A logical formation number is a number starting from 0 that identifies members from front to back (so 0 is frontmost, 1 is one position back and 2 is 2 positions back). On its own, this doesn't do anything, but the caller (in this case, BattleControl's SwitchPos or SwitchParty) will go further and physically move each playerdata
to their party position using battle.partypointer
as the mapper.
So for example, to move the front member to its position after calling SwitchParty(true), BattleControl will set playerdata[partypointer[0]]
.position = partypos[0]
. This will work regardless of how the switch was done and when this is done on all playerdata
elements using matching playerdata
indexes, it will move all party member to their respective spot because battle.partypointer
is the mapper that dictates everything here. Additionally, it also updates the BattleData's pointer
to the matching partypointer
, but this always result in being the same as the playerdata
index.
What's important to mention here is that unlike overworld switching, the party members actually moves between each other, but the internal playerdata
ordering will always remain in trueid
order no matter what overworld order the party was in when StartBattle was called and no matter how many switches or swaps of positions happens during the battle.
This property is enforced by StartBattle: it will arrange the party in trueid
order initially, but then performs a series of SwitchParty(true) until the party is aligned with the overworld party order. This implies that battle.partypointer
will map to the same overworld party order in battle when enumerating playerdata[battle.partypointer[i]]
elements sequentially, but further switches will only cause changes to battle.partypointer
.
This way of enumerating the party gives us the battle formation order: it allows to enumerate the party by their battle formation position from front to back (right to left). For example, playerdata[partypointer[0]]
always refer to the party member in the front.
This mechanism is also what allows SwitchPos to work: instead of cycling battle.partypointer
, all that's needed to swap 2 members is to simply swap the 2 elements in battle.partypointer
and refresh everyone's position in battle formation order.
Additionally, there's an interesting side effect to all of this: in battles, any BattleData's trueid
will match their battleentity
.animid
. This is because StartBattle initially composed the battle entities in trueid
order and only did SwitchParty(true) to make it align with the overworld party order, but that doesn't change any animid, only their physical positions and the battle.partypointer
mapper. This means that in battles specifically, addressing by trueid
can be used to address by animid and it is the most reliable way to address a specific member.
However, this method of party ordering has a downside: it now means that BattleData's animid
are no longer valid during a battle. This is because battle switching has no animid cycling so it doesn't need to use it and their values only reflect the overworld which the battle system disociated on StartBattle. It means that it's not valid to address the party in overworld party order during a battle. This is usually fine because trueid
order and battle formation order are plenty to address everyone by animid or by formation position.
ChangeParty¶
Now that we're established how the party can be addressed, let's talk about the methods involved in managing the party and their stats.
Starting with the most important method that setup the party: ChangeParty:
public static void ChangeParty(int[] ids, bool fromscratch)
public static void ChangeParty(int[] ids, bool fromscratch, bool destroyoldentity)
This method is complex, but in general, it destructively reorganise the party in playerdata
using ids
as the animids where their order determines the trueid
order. This method is the only way to change the trueid
order. This is a desructive operation: it completely invalidates previous playerdata
references and should only be done in a controlled manner such as specific events or when loading the save.
The fromscratch
should always be true. If it's false, the behavior can be considered broken because the method won't actually change the party and it will instead result in playerdata
being reset to a new list, but leave partyorder
to ids
which wouldn't be correct since the method didn't actually change the party. The only time the game sends false to this parameter is when loading the save file, but it doesn't actually do anything useful because the loading process sets playerdata
to a new list anyway.
Finally, the destroyoldentity
parameter will cause the older playerdata
's overworld entities that aren't present in the new party to be destroyed instead of being appended to map.entities
. NOTE: This functionality of determining removed party members is broken, see the section below for details.
The overload without a destroyoldentity
parameter has its value default to true.
Here's how the method process goes (fromscratch
is assumed to be true since a false value leads to a broken party change):
- If
playerdata
isn't null or empty, all of theentity
are saved in an array for reference later partyorder
is set toids
playerdata
is set to a new array containing the return values of SetDefaultStats using eachids
elements in order (the method is detailed more below, but it notably sets thetrueid
and BattleData'sanimid
to the matchingids
element)- Each entity in list saved earlier that aren't null are checked to see if they are to be removed from the party or stayed. NOTE: The determination logic is incorrect, check the section below for details. What happens depends if it was found the entity is removed from the new party or not:
- If it's not removed, the matching
playerdata
'sentity
is set to it. Their tag is set toPlayer
with a layer of 10 () forplayerdata[0]
orPFollower
with a layer of 9 () for otherplayerdata
- If it is removed, what happens depends on
destroyoldentity
:- If it's true, the entity is destroyed
- If it's false and the map isn't null (nothing happens if it is null), then the entity gets childed to the
map
with a tag ofUntagged
and it's appended to the end ofmap.entities
after reassigning it to a new arrays
- If it's not removed, the matching
- If the older party wasn't empty and the new party isn't empty, but
playerdata[0].entity
doesn't have a PlayerControl, one will be added to it followed by a destruction ofplayer
(it will get automatically reasigned by PlayerControl's Start) - ApplyBadges is called
- ApplyStatBonus is called
- All
playerdata
'shp
is set to theirmaxhp
- RebuildHUD is called
Problem with party members removal¶
As explained above, ChangeParty should normally determine which party members is getting removed compared to the older party so it can either destroy the entity or move it out of the way. The problem is the logic to determine this is bogus and can't be relied upon.
What should be happening is figuring out if the older party contains all elements in ids
and if it doesn't, the entities of the members that the new party has in excess should be destroyed / moved.
What is instead happening is only playerdata[0].animid
is checked for this against the older entities's animids. It means that for any older party members whose entity.animid
isn't playerdata[0].animid
, it is incorrectly deemed as a removed party member even though they might appear in the party in a further playerdata
element.
Even more confusingly, because it's based on BattleData's animid, this is checked in older overworld party order. This means that even overworld switches before the ChangeParty call can influence this behavior. As a general rule however, this issue only has a bad effect on a ChangeParty where ids
has more than one element.
A workaround to this issue is to not rely on the proper member's entities composition and just call SetPlayers after the ChangeParty. This will recreate every entity
of every BattleData with the proper animid derived from the BattleData one. It is considered undefined to not do this after a ChangeParty because it may lead to overworld entities being incorrectly destroyed and left to null.
SwitchParty¶
This method rotates the party where each member cycles to the position in front of them except for the front member which cycles to the backmost position. Here is its signature:
public static void SwitchParty(bool battle)
The battle
parameter determines the kind of switching: true means a battle switch while false means an overworld switch. They behave very differently with each other with a high level explanation mentioned in a section above. It should never be called with a value of true while MainManager.battle
is null or an exception will be thrown.
The method does nothing for an overworld switch while inevent
.
Here is the detailed process for an overworld switch:
- PlaySound(
Switch
) called - All
playerdata
'sanimid
are rotated with each other such thatplayerdata[X].anim
becomesplayerdata[X + 1].anim
whereX
is anyplayerdata
index except for the last one where its animid will becomeplayerdata[0].anim
switchicon
(if it exist) gets its sprite changed toguisprites[playerdata[0].animid + 94]
which is the icon sprite whose animid is the frontmost member with a color of full white, but half transparentplayerdata[0].entity.emoticonoffset
(the frontmost member'sentity
.emoticonoffset
) is set to (0.0, 1.8 + 0.25 * their BattleData'sanimid
, -0.1)
Here is the detailed process for a battle switch:
- All
battle.partypointer
elements are rotated with each other such thatbattle.partypointer[X]
becomesbattle.partypointer[X + 1]
whereX
is anyplayerdata
index except for the last one where the number becomesbattle.partypointer[0]
Other party utilities¶
The following are a bunch of potentially useful utilities to work with playerdata
and its BattleData as a whole. This doesn't include the methods that has to do with the party stats as these are brought up in a section below.
public static bool HasPlayer(int id)
Returns true if any playerdata
has a trueid
of id
, false otherwise.
public static BattleData GetPlayerData(int id, bool frombattleentity)
public static BattleData GetPlayerData(int id)
Returns the first playerdata
whose trueid
is id
or the first playerdata
if none exists.
If frombattleentity
is true however, the method will first search for a playerdata
whose battleentity
has a battleid
of id and it will return it if one is found. Otherwise, the regular search is done.
On the overload that doesn't take a frombattleentity
, the default value is false.
public static BattleData? GetPlayerDataNullable(int id)
Returns the first playerdata
whose trueid
is id
or null if none exists.
public void RefreshPlayer(bool onlycollider = false)
Reset various physical related state to the player
and playerdata
:
player
.ceiling
is set to false- If
onlycollider
is false, everyplayerdata
whoseentity
.noclock
is false gets their entity rooted to the scene and theirentity
.onground
set to false. NOTE: This strangely causes any player entities to be off the ground even if their ground detector disagrees so the overall effect is they are incorrectly marked as in the air for the frame this method is called frame. - If the game isn't in a
battle
and notinevent
, ForceHitWall is called onplayer
.entity
public static bool FreePlayer(bool getfly)
public static bool FreePlayer()
Only returns true if all of the following conditions are fufilled (returns false otherwise):
player
exists (meaning a map isn't being loaded)- The game isn't in a
minipause
- The game isn't in a
pause
- The game isn't
inevent
- The SetText
message
lock isn't active - The
player
isn'tdigging
or using the submarine's sonar - If
getfly
is true, theplayer
also needs to not beflying
(otherwise, this condition doesn't apply)
On the overload without getfly
, the default value is true.
public static bool IsPlayerInPos(int pos, Transform entity)
Returns true if playerdata[battle.partypointer[pos]]
.battleentity
is entity
, false otherwise.
public static bool IsParty(Transform obj)
Returns true if obj
is a playerdata
's entity
's transform, false otherwise.
public static bool PartyIsNotMoving()
Returns true if no playerdata
's entity
are in a forcemove
, false otherwise.
public static int[] PartyArray()
This is UNUSED. Returns an array of a single element being playerdata[0]
.trueid
.
public static void SetPlayers()
public static void SetPlayers(Vector3[] newentitypos)
A part of the map loading process. Recreate all playerdata
elements. If newentitypos
isn't null, the existing playerdata
gets destroyed before the recreation and after, their position are set to matching newentitypos
elements (indexed by playerdata
index).
The parameterless overload acts as if newentitypos
was null.
The recreation process is documented in the map loading documentation, but the method is also used in certain events that needs to recreate the party because as explained above, pairing a call with ChangeParty is the best way to guarantee the integrety of the overworld entities of the party.
public static EntityControl[] GetPartyEntities()
public static EntityControl[] GetPartyEntities(bool idorder)
Returns an array of all playerdata
's entity
. If idorder
is false, the array is ordered by playerdata
index. If idorder
is true, the order is done by animid of Bee
, Beetle
and Leif
(more precisely, it's GetEntity(-4), GetEntity(-5) and GetEntity(-6)).
public static Vector3[] GetPartyPos(bool inorder)
Return the playerdata
positions in an array. If inorder
is false, they are ordered by playerdata
index. If inorder
is true, they are in the following order: GetEntity(-4), GetEntity(-5), GetEntity(-6).
public static void DestroyPlayers(bool remake, bool inorder)
This is UNUSED, but remains functional. Destroys all playerdata
's entity
.gameObject. If remake
is true, SetPlayers(GetPartyPos(inorder
)) is called after.
public static void StopDig()
public static void StopDig(bool delayed)
Does the following on each playerdata
's entity
which ends any digging animations that were ongoing:
- Call LockRigid(false) on the entity
- Reset
spin
to Vector3.zero - Reset
startscale
to Vector3.one - Reset the
sprite
's scale to Vector3.one - Reset
overrideanim
to false - Enables the
ccol
The overload with a delayed
parameter is UNUSED, but remains functional and it will start a DigStop coroutine instead, but all it does is yield for 0.1 seconds before calling the other overload so it's effectively doing the same after waiting for 0.1 seconds.
private static IEnumerator DigStop()
Part of the UNUSED StopDig overload where it will yield for 0.1 seconds before calling the parameterless overload of StopDig.
Party stats¶
The way the party stats are calculated is complex because it's a manual process that involves multiple components being added together.
As explained in ChangeParty above, there's 3 methods it ends up calling that messes with the new BattleData and these 3 methods when called together allows to calculate the effective stats of the party:
- SetDefaultStats (needs to be called on all
trueid
of the party which ChangeParty normally does). This NEEDS to be called to initialise some fields - ApplyStatBonus (operates on the whole party). It applies all
statbonus
- ApplyBadges (operates on the whole party). It applies all BadgeEffects
Some of these methods ends up doing similar steps than the others and some even calls the other methods, but it doesn't have negative effects to do so because they are built in a way that ensure the correct stats numbers are set by the end. There is one requirement: both ApplyBadges and ApplyStatBonus needs to be called to guarantee a full recalculation of the party stats (order doesn't matter). Even if it results in a double call, these methods are resilient to this because they reset their numbers before proceeding.
To summarise the best way to call these methods:
- Calling ChangeParty (with
fromscratch
set to true) guarantees the party stats are fully calculated so calling any of these methods after isn't necessary - Once medals equipment changes occurs that implicate changes to stats, ApplyBadges should be called
- Once
statbonus
changes happen, ApplyStatBonus should be called
SetDefaultStats¶
Mentioned in the ChangeParty section is a method used to build the basic fields of the new BattleData. That method is SetDefaultStats:
private static BattleData SetDefaultStats(int id)
It returns a BattleData with the following fields:
animid
andtrueid
:id
hp
,maxhp
andbasehp
: 7 (9 ifid
is 1 which isBeetle
)atk
andbaseatk
: 2skills
: new empty listcursoroffset
: 2.3 in yentityname
:menutext
at index 46 +id
(this is basically the language specific party member name ofBee
,Beetle
andMoth
)condition
: new empty list
This essentially initialises the basic fields so the BattleData is ready for use. Notably, it is the only method that sets trueid
.
ChangeParty should always call this. The only time it doesn't is when the game setup the party when loading the save file, but it ends up doing the same important steps.
ApplyStatBonus¶
This method is what ends up applying the statbonus
which are permanent stats upgrades the party accumulated through various means. They are saved on the save file with a specific format documented in the save file format documentation.
Here is its signature:
public static void ApplyStatBonus()
It does the following to apply all of the statbonus
on top of the base party stats:
- Calls ResetStats
- Apply all of the
statbonus
(check the save file documentation for the formatting) - If
statbonus
wasn't empty, calls ApplyBadges
The way it works is it only messes with the following fields:
- All
playerdata
'sbasehp
- All
playerdata
'sbaseatk
- All
playerdata
'sbasedef
- The party's
basetp
These "base" fields aren't actually used for anything in the game except for stats calculation since they represents the stats after statbonus
are applied, but before BadgeEffects are applied. Most notably, it's only useful for ApplyBadges to use these numbers as a base, but this is taken into account because if there was any statbonus
, ApplyBadges gets called. Not having any statbonus
would still mean that ApplyBadges would need to get called, but it doesn't matter if it was done before or after since ApplyStatBonus wouldn't have done anything in that case.
As for ResetStats, it's a simple method that ensures the "base" fields are reset to their base value before the procedure:
private static void ResetStats()
It resets every playerdata
's basehp
, baseatk
and basedef
to their base value and reset basetp
to its base value. The base values are the following:
basehp
: 7 (9 if theplayerdata
'strueid
is 1 which isBeetle
)baseatk
: 2basedef
: 0basetp
: 10
This is why ApplyStatBonus can be called multiple times without negative effects.
ApplyBadges¶
ApplyStatBonus isn't enough on its own because it only applies statbonus
, but it doesn't consider medals which have effects on stats. Those effects are called BadgeEffects which are defined in the medals data, check the medals data documentation to learn more.
ApplyBadges is the method that will apply these effects on top of the base stats:
public static void ApplyBadges()
Here's the exact procedure:
- Set instance.
speedup
(an UNUSED field) to true - Set instance.
maxtp
to instance.basetp
. - Call ResetPlayerStats is called on player party member (more details below)
- All the
BadgeEffects
of every applicable medals are applied just as described in the medals data documentation. - instance.
tp
is clamped from 0 to instance.maxtp
- All
playerdata
'shp
is clamped from 0 to theirmaxhp
As for ResetPlayerStat, it's a simple method that ensures the stats fields are reset to their initial value before the procedure:
private static void ResetPlayerStat(int id)
More precisely, it sets these following fields:
Field | Value set |
---|---|
lockitems |
false |
lockskills |
false |
locktri |
false |
lockrelayreceive |
false |
maxhp |
basehp |
atk |
baseatk |
def |
basedef |
poisonres |
0 |
sleepres |
0 |
freezeres |
0 |
numbres |
0 |
Other party stats utilities¶
Here are other useful party stats related methods:
public static void AddStatBonus(StatBonus type, int ammount, int to)
Adds a new element to the statbonus
list using the type
, ammount
and to
values provided. Check the save file documentation for what these values mean.
public static void Heal()
public static void Heal(bool noparticle, bool nosound)
public static void Heal(Healing[] parameters, int[] partyids, bool noparticle, bool nosound)
Heals the playerdata
whose index is in partyids
where the healing is determined by parameters
and perform the associated visual / audio effects for the healing. If nosound
is true, PlaySound(Heal
) will not be called. If noparticle
is true, HealParticle will never be called even if it would have been otherwise. This method also sets hudcooldown
to 100.0 to show the party HUD temporarilly. If HealParticle gets called, the parent is the playerdata
's entity
or battleentity
depending if the game is in a battle
.
For the first 2 overloads, the parameters
value defaults to a single element being Full
, partyids
defaults to partyorder
(which is every playerdata
), noparticle
defaults to false and nosound
defaults to false.
Here are the healing effects of the different parameters
value:
Full
: Theplayerdata
'shp
is set theirmaxhp
and the party'stp
is set tomaxtp
. Particles are included ifnoparticle
is falseTPOnly
: The partytp
is set tomaxtp
. Particles are always excluded regardless ofnoparticle
FullHPOnly
: Theplayerdata
'shp
is set theirmaxhp
. Particles are included ifnoparticle
is false
public static IEnumerator LevelUpMessage()
Applies any rank up bonuses if any exists and recalculate the party stats. Check the LevelUpMessage documentation to learn more.
public static bool PartyLowHP()
UNUSED: No one calls this, but it remains functional.
Returns true if any playerdata
has an hp
of 4 or below, false otherwise.