Basic knowledge to the default RMMZ TPBS battle flow implementations

Started by DoubleX, March 05, 2022, 02:00:31 am

Previous topic - Next topic

DoubleX

This topic aims to share the basic knowledge on what the default RMMZ TPBS battle flow implementations do in general, but you're still assumed to have at least:
1. Some plugin development proficiency(having written several easy, simple and small battle-related plugins up to 1k LoC scale)
2. Basic knowledge on what the default RMMZ turn based battle flow implementations do in general
3. Basic knowledge on what the default RMMZ TPBS battle flow does in general on the user level(At least you need to know what's going on on the surface when playing it as a player)

Please note that this flowchart only includes the most important elements of the battle flow, to avoid making it too complicated and convoluted for the intended targeting audience

"Battle Start": ShowHide

It's almost exactly the same as that in the turn based counterpart, so the phase will change to "start" after finished starting the battle.


"Input Action Slots": ShowHide

It's actually quite similar to that in the turn based counterpart, except that:

1. As nothing other than inputting action slots can happen during the "input" phase in the turn based counterpart, the exact input sequence is always fixed there, whereas in the TPBS some party members can become inputable or uninputable at anytime, especially when it comes to active TPBS.

2. At any given frame in TPBS, players are still supposed to start from inputting the 1st action slot of the 1st inputable party member to inputting the last action slot of the last party member.

3. If players are inputting the action slots of a party member, and then they cancel inputting all those action slots of of that party member, among all the other inputable party members, those whose party member indices are greater rather than less than the previously inputting one will be selected first, and the party command window will be setup in case no other inputable party member exists.
This is because BattleManager.selectPreviousCommand will call BattleManager.selectPreviousActor, which will still call BattleManager.changeCurrentActor with argument forward as true, which is the same as what BattleManager.selectNextActor, which is called by BattleManager.selectNextCommand, does:
BattleManager.selectPreviousCommand = function() {
    if (this._currentActor) {
        if (this._currentActor.selectPreviousCommand()) {
            return;
        }
        this.cancelActorInput();
    }
    this.selectPreviousActor();
};

4. Also, unlike the turn based counterpart, if another inputable party member is selected for inputting actions due to cancelling inputs of another inputable party member, the newly selected one will still input his/her/its 1st action slot first, then proceed the same sequence until the last action slot is inputted.

5. As a corollary, once an inputable party member has inputted all his/her/its action slots, there's no way to cancel those input because he/she/it'll proceed to casting and then executing those actions, and this is unlike the turn based counterpart, where players can cancel party members who've inputted all their action slots, as long as the phase is still "input".

6. In the turn based counterpart, the only way to activate or deactivate any input window is by the ok and cancel commands issued by players, whereas in TPBS this can also be affected by whether the currently inputting party member becomes not inputable, due to Scene_Battle.prototype.changeInputWindow, which will only be called if some input windows need to be activated/deactivated:
Scene_Battle.prototype.changeInputWindow = function() {
    this.hideSubInputWindows();
    if (BattleManager.isInputting()) {
        if (BattleManager.actor()) {
            this.startActorCommandSelection();
        } else {
            this.startPartyCommandSelection();
        }
    } else {
        this.endCommandSelection();
    }
};
In short, other than hiding the skill, item, actor and enemy windows:
- If there are inputable party members and 1 of them becomes selected to input action slots, the actor command window will be setup with the status window selecting that actor
- If there are inputable party members and all of them become not selected to input action slots, the party command window will be setup with the status window deselected
- If there are no more inputable party members, all the input windows will be closed and the status window will be deselected

Bear in mind that how the above points work in details are too advanced to be covered here.


"Thinking In Frames": ShowHide

Unlike the default turn based battle system, thinking in frames are vital even in just trying to have a basic knowledge on what the default RMMZ TPBS battle flow implementations do in general, especially when it comes to active TPBS, so the flowchart is drawn quite differently from the turn based counterpart.
To be able to think in frames, one first need to know the starting point of a frame and all the possible ending points of that frame, then one can sequentially grasp the summary of each path, until a vague impression of a frame can be formed.
To make the task even easier, simpler and smaller, one can first try to briefly read the codes without thinking about active TPBS, which is more complicated and convoluted, especially when it comes to edge cases that are hard but still possible to reach.
When one becomes familiar with thinking in frames, he/she should be able to at least partially simulate what a frame does in general in his/her mind, and eventually roughly visualize the TPBS battle flow implementations mentally.


"Frame Start": ShowHide

A frame starts from Scene_Battle.prototype.update, which is a vital part of the scene life cycle(too advanced to be covered here):
Scene_Battle.prototype.update = function() {
    const active = this.isActive();
    $gameTimer.update(active);
    $gameScreen.update();
    this.updateVisibility();
    if (active && !this.isBusy()) {
        this.updateBattleProcess();
    }
    Scene_Message.prototype.update.call(this);
};

Then Scene_Battle.prototype.updateBattleProcess will be called to use the result of Scene_Battle.prototype.isTimeActive as the argument of BattleManager.update, which is being called immediately afterwards:
Scene_Battle.prototype.updateBattleProcess = function() {
    BattleManager.update(this.isTimeActive());
};
Scene_Battle.prototype.isTimeActive = function() {
    if (BattleManager.isActiveTpb()) {
        return !this._skillWindow.active && !this._itemWindow.active;
    } else {
        return !this.isAnyInputWindowActive();
    }
};
BattleManager.update = function(timeActive) {
    if (!this.isBusy() && !this.updateEvent()) {
        this.updatePhase(timeActive);
    }
    if (this.isTpb()) {
        this.updateTpbInput();
    }
};
Because of Scene_Battle.prototype.isTimeActive, the active TPBS will keep the TPB running unless the skill or item window's active, while the non-active TPBS will only keep the TPB running when there are no active input windows(party or actor command, or skill, item, actor or enemy window), meaning that there are no inputable party members.
(On a side note: Strictly speaking, the way the TPBS battle flow's implemented won't let plugin developers change the active TPBS to keep the TPB running even when battlers are executing actions, unless those plugin developers rewrite the whole TPBS from scratch, but these details are way, way too advanced and complex to be elaborated here)

BattleManager.isBusy and BattleManager.updateEvent will be called to only call BattleManager.updatePhase when the TPB can technically keep running(the details of these underlying technical limitations are way, way too advanced and complex to be elaborated here):
BattleManager.isBusy = function() {
    return (
        $gameMessage.isBusy() ||
        this._spriteset.isBusy() ||
        this._logWindow.isBusy()
    );
};
BattleManager.updateEvent = function() {
    switch (this._phase) {
        case "start":
        case "turn":
        case "turnEnd":
            if (this.isActionForced()) {
                this.processForcedAction();
                return true;
            } else {
                return this.updateEventMain();
            }
    }
    return this.checkAbort();
};
BattleManager.updatePhase = function(timeActive) {
    switch (this._phase) {
        case "start":
            this.updateStart();
            break;
        case "turn":
            this.updateTurn(timeActive);
            break;
        case "action":
            this.updateAction();
            break;
        case "turnEnd":
            this.updateTurnEnd();
            break;
        case "battleEnd":
            this.updateBattleEnd();
            break;
    }
};
While Game_Message.prototype.isBusy and Spriteset_Battle.prototype.isBusy are self-explanatory enough, Window_BattleLog.prototype.isBusy is a lot more complicated and convoluted(too advanced to be covered here), but it's still about whether the TPB needs to stop to let the visual coordination running, like the battle log, animations, battler sprites, etc.
The main function of interest inside BattleManager.updateEvent is BattleManager.updateEventMain(too advanced to be covered here), and what makes it interesting here is that it'll check whether the battle needs to end by checking whether it's aborted, victorious or defeated, and will change the phase to "battleEnd" if any of those conditions are met.
As for BattleManager.updatePhase, it's mainly about picking the function to call according to the current phase of the battle, while the argument timeActive is the result of Scene_Battle.prototype.isTimeActive.


"Start Phase": ShowHide

There's not much in this phase, as all BattleManager.updateStart does in TPBS is to change to phase to "turn":
BattleManager.updateStart = function() {
    if (this.isTpb()) {
        this._phase = "turn";
    } else {
        this.startInput();
    }
};


"Turn Phase": ShowHide

The "turn" phase is the majority of the difference between the TPBS battle flow and the turn based counterpart.
First, BattleManager.updateTurn will be called to use the argument timeActive as the result of Scene_Battle.prototype.isTimeActive to determine if BattleManager.updateTpb should be called as well:
BattleManager.updateTurn = function(timeActive) {
    $gameParty.requestMotionRefresh();
    if (this.isTpb() && timeActive) {
        this.updateTpb();
    }
    if (!this._subject) {
        this._subject = this.getNextSubject();
    }
    if (this._subject) {
        this.processTurn();
    } else if (!this.isTpb()) {
        this.endTurn();
    }
};
BattleManager.updateTpb = function() {
    $gameParty.updateTpb();
    $gameTroop.updateTpb();
    this.updateAllTpbBattlers();
    this.checkTpbTurnEnd();
};
Assuming that timeActive is true -

Now, Game_Unit.prototype.updateTpb will be called to call Game_Battler.prototype.updateTpb for all battlers:
Game_Battler.prototype.updateTpb = function() {
    if (this.canMove()) {
        this.updateTpbChargeTime();
        this.updateTpbCastTime();
        this.updateTpbAutoBattle();
    }
    if (this.isAlive()) {
        this.updateTpbIdleTime();
    }
};
So, if a battler can move, he/she/it'll update the TPB and action casting bars, as well as start casting all the autobattle actions that are just made in case he/she/it's in Auto Battle.
If he/she/it's alive, he/she/it'll update the idle TPB bar as well.
If his/her/its TPB becomes fully charged, he/she/it'll become available for inputting action slots.
If his/her/its action casting bar becomes full, he/she/it'll become available for executing valid actions.

BattleManager.updateAllTpbBattlers will call BattleManager.updateTpbBattler for all battle members:
BattleManager.updateTpbBattler = function(battler) {
    if (battler.isTpbTurnEnd()) {
        battler.onTurnEnd();
        battler.startTpbTurn();
        this.displayBattlerStatus(battler, false);
    } else if (battler.isTpbReady()) {
        battler.startTpbAction();
        this._actionBattlers.push(battler);
    } else if (battler.isTpbTimeout()) {
        battler.onTpbTimeout();
        this.displayBattlerStatus(battler, true);
    }
};
First, if the turn of the battler involved becomes ended, the old turn will be ended and the new one will be started here, with the latest battler status displayed on the battle log window.
Second, if the battler involved becomes available for executing actions, that battler will be pushed into the back of the action execution subject queue, so later BattleManager.updateTurn can call BattleManager.getNextSubject to pickup that battler to be the action execution subject.
Third, if the battler involved has become idled for so long that a turn has passed, that battler will be in the new battler turn, with the latest battler status displayed on the battle log window.

BattleManager.checkTpbTurnEnd will be covered in "Turn End Phase".

Regardless of whether BattleManager.updateTpb is called, the rest of BattleManager.updateTurn is exactly the same as the turn based counterpart.


"Action Phase": ShowHide

It's almost the same as the turn based counterpart, as least when only the battle flow is concerned.


"Turn End Phase": ShowHide

It's quite similar to the "turn" phase in TPBS, except that, after calling BattleManager.checkTpbTurnEnd, if Game_Troop.prototype.isTpbTurnEnd returns true, BattleManager.endTurn will be called to change the phase to "turnEnd" as well:
BattleManager.checkTpbTurnEnd = function() {
    if ($gameTroop.isTpbTurnEnd()) {
        this.endTurn();
    }
};
Game_Troop.prototype.isTpbTurnEnd = function() {
    const members = this.members();
    const turnMax = Math.max(...members.map(member => member.turnCount()));
    return turnMax > this._turnCount;
};
BattleManager.endTurn = function() {
    this._phase = "turnEnd";
    this._preemptive = false;
    this._surprise = false;
};
Note that this doesn't always mean that the phase at the next frame will be "turnEnd", because as shown in the flowchart, it's still possible that BattleManager.startAction will be called to change the phase to "action" before proceeding to the next frame(the proof of this possibility is too advanced to be covered here), meaning that the battle turn count can trigger later than expected, and thus potentially surprising effects on the subsequent action executions before all the queued action execution subjects have executed all their valid actions.


"Battle End Phase": ShowHide

It's exactly the same as the turn based counterpart as well, since BattleManager.updateBattleEnd is the absolute last stop of both of the battle flows:
BattleManager.updateBattleEnd = function() {
    if (this.isBattleTest()) {
        AudioManager.stopBgm();
        SceneManager.exit();
    } else if (!this._escaped && $gameParty.isAllDead()) {
        if (this._canLose) {
            $gameParty.reviveBattleMembers();
            SceneManager.pop();
        } else {
            SceneManager.goto(Scene_Gameover);
        }
    } else {
        SceneManager.pop();
    }
    this._phase = "";
};
Note that SceneManager here is to change the scene from Scene_Battle to something else:
1. Exits the game in the case of battle test
2. Goes to the last scene(the one before this battle) if it's not a game over
3. Goes to the game over scene(Scene_GameOver)


"Update TPB Input": ShowHide

It's always run at the end of a frame in TPBS, regardless of what the current phase of the battle is.
Basically, if there's at least 1 inputable party members, BattleManager.updateTpbInput will call BattleManager.checkTpbInputClose, otherwise it'll call BattleManager.checkTpbInputOpen:
BattleManager.updateTpbInput = function() {
    if (this._inputting) {
        this.checkTpbInputClose();
    } else {
        this.checkTpbInputOpen();
    }
};
BattleManager.checkTpbInputClose = function() {
    if (!this.isPartyTpbInputtable() || this.needsActorInputCancel()) {
        this.cancelActorInput();
        this._currentActor = null;
        this._inputting = false;
    }
};
BattleManager.checkTpbInputOpen = function() {
    if (this.isPartyTpbInputtable()) {
        if (this._tpbNeedsPartyCommand) {
            this._inputting = true;
            this._tpbNeedsPartyCommand = false;
        } else {
            this.selectNextCommand();
        }
    }
};

In the case of running BattleManager.checkTpbInputClose, it's to void the currently inputting party member(the one whose action slots are being inputted by players) and the party inputability flag if the currently inputting party member becomes not inputable(BattleManager.isPartyTpbInputtable is mainly for handling edge cases here).

In the case of BattleManager.checkTpbInputOpen, the gist is that(the details are too advanced to be covered here), when at least 1 of the party members become inputable, the party inputability flag will be raised if it's the 1st time the party becomes inputable(to show the party command window instead of the actor command window), otherwise BattleManager.selectNextCommand will be called:
BattleManager.selectNextCommand = function() {
    if (this._currentActor) {
        if (this._currentActor.selectNextCommand()) {
            return;
        }
        this.finishActorInput();
    }
    this.selectNextActor();
};
While the exact mechanism of raising the inputability flag and setting up the actor command window are too advanced to be covered here, the point is that BattleManager.selectNextActor will call BattleManager.changeCurrentActor, which will call BattleManager.startActorInput, and thus raise the inputability flag if the players are already inputting the action slots of an inputable party member.


"Summary": ShowHide

First, the battle will start, and the phase will change to "start" upon fully starting the battle, with the catch that this phase will only trigger once, which is at the start of the battle.
Then, the phase will quickly change to "turn".

After that, all battlers will charge their TPB, and will become inputable when theirs become full.
In the case of actors, the party command window will be setup for the 1st time such event triggers in this battle, otherwise the actor command window corresponding to the inputable party member with the smallest party index at that frame will be setup.
Whenever a battler becomes restricted, his/her/its TPB and cast bars will be cleared.
Players will input from the 1st action slot of the 1st inputable party member to the last action slot of the last inputable party member at any given frame.
Whenever the party becomes to have at least 1 inputable party member, the actor command window will be setup if an actor's selected for inputting action slots, otherwise the party command window will be setup.
Whenever the party becomes to have no inputable party members, all the input windows will be closed.

When battlers finish inputting all their action slots, they'll start casting those actions, until they're fully cast, and this will cause those battlers to be pushed at the back of the action execution subject queue.
As long as no actions are already executing, the most up front battler in that queue will be picked up as the action execution subject to execute will be cast valid actions, and the phase will be changed to "action".

When that action execution subject has executed all those cast valid actions, that battler will have the TPB and cast bars emptied, and the above process of picking up new action execution subject will be repeated, until there are no more battlers available as action execution subjects, in which the phase will be changed to "turn".

If a battle turn's supposed to be ended, the phase will be changed to "turnEnd", but it'll be immediately changed to "action" at the same frame if there are still action execution subjects to execute actions.

If a battle's supposed to be ended, the phase will be changed to "battleEnd", and the scene will be changed from the battle scene to something else, followed by changing the phase to empty.


That's all for now. I hope this can help you grasp these basic knowledge. For those thoroughly comprehending the essence of the default RMMZ TPBS battle flow implementations, feel free to correct me if there's anything wrong
For those wanting to have a solid understanding to the default RMMZ TPBS battle flow implementations, I might open a more advanced topic for that later
My RMVXA/RMMV/RMMZ scripts/plugins: http://rpgmaker.net/users/DoubleX/scripts/