Alliance
The Alliance screen shows the allies the player owns and lets the player pick the ally to take into battle, level an ally with EXP items, upgrade its Weapon and reset its level. Levels, costs, stats and refunds are decided by the server.
| You use | It gives you |
|---|---|
IProjectVAllianceScreen | The screen contract: observe the allies, select, level up, upgrade, reset |
ProjectVAllianceView | What to draw: loading state, allies, the selected ally, EXP items |
ProjectVAllyView | One ally: level, EXP, stats, Weapon skill, previews |
ProjectVXpItemView | One EXP item and how many the player owns |
ProjectVCharacterXpItemSelection | The EXP items and quantities to spend |
ProjectVResult | The outcome of a command |
Allies use the same stats, EXP items and Weapon skills as the Character screen.
Quick startโ
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SilverTiger.ProjectV.Unity.Presentation;
using SilverTiger.ProjectV.Unity.Presentation.Alliance;
using SilverTiger.ProjectV.Unity.Presentation.Character;
using UnityEngine;
using VContainer;
public sealed class AlliancePage : MonoBehaviour
{
private IProjectVAllianceScreen alliance = null!;
private IDisposable? observation;
[Inject]
public void Construct(IProjectVAllianceScreen screen) => alliance = screen;
private void OnEnable() => observation = alliance.Observe(Render);
private void OnDisable()
{
observation?.Dispose();
observation = null;
}
private void Render(ProjectVAllianceView view)
{
if (view.IsLoading)
{
ShowLoading(view.LoadError); // LoadError is null while the first load is running
return;
}
ShowAllies(view.Allies, view.SelectedAllyId);
ShowXpItems(view.XpItems);
}
public void OnSelectClicked(string allyId) => _ = RunAsync(alliance.SelectAllyAsync(allyId));
// The "+1" button; use XpForMaximumLevel() for "Max".
public void OnLevelUpClicked(ProjectVAllyView ally) => _ = RunAsync(alliance.LevelUpAsync(ally.Id, ally.XpForNextLevel()));
public void OnWeaponUpgradeClicked(string allyId) => _ = RunAsync(alliance.UpgradeWeaponSkillAsync(allyId));
public void OnResetClicked(string allyId) => _ = RunAsync(alliance.ResetLevelAsync(allyId));
private async Task RunAsync(Task<ProjectVResult> command)
{
var result = await command;
if (this != null && !result.IsSuccess)
{
ShowMessage(result.Message);
}
}
private void ShowLoading(string? error) { }
private void ShowAllies(IReadOnlyList<ProjectVAllyView> allies, string? selectedAllyId) { }
private void ShowXpItems(IReadOnlyList<ProjectVXpItemView> items) { }
private void ShowMessage(string message) { }
}
That is the whole page: observe, draw, call a command. After a success you do
nothing; Render is called again with the new data.
What to drawโ
Render receives a ProjectVAllianceView:
| Member | Type | Use it for |
|---|---|---|
IsLoading | bool | True until the allies arrive the first time. Show a spinner |
LoadError | string? | Set when that first load failed. Show it with a Retry button that closes and reopens the page |
Allies | IReadOnlyList<ProjectVAllyView> | The allies the player owns |
SelectedAllyId | string? | The ally the player takes into battle; null when none is selected. Highlight from this, not from a local flag |
XpItems | IReadOnlyList<ProjectVXpItemView> | EXP items: ItemId, Experience (EXP per item), Owned |
PendingUpgradeAllyId | string? | The ally whose level-up is not confirmed yet |
Find(allyId) | ProjectVAllyView? | Look up one ally, for example to refresh an open upgrade panel |
For each ProjectVAllyView:
| To show | Read |
|---|---|
| Level and EXP | Level, Experience; Upgrade for what the next level needs |
| Stats | Stats (health, attack, armor and so on), as on Character |
| Max level | IsMaxLevel |
| The "+1" and "Max" selections | XpForNextLevel(), XpForMaximumLevel(); empty when no level can be reached |
| What a selection would do | PreviewXp(items): CanSubmit, Message, GrantedExperience, TargetLevel |
| The Weapon upgrade | WeaponSkill: Level, MaxLevel, Costs, CanUpgrade, BlockedMessage |
| The reset refund | LevelReset: CanReset, Message, Refunds |
| "Finishing upgrade" | HasPendingUpgrade: this ally's level-up is not confirmed yet |
| Level buttons disabled | IsBlockedByPendingUpgrade: another ally's level-up is not confirmed yet |
The view is always built from the last confirmed data, so previews do not
change while a command is running. Match an ally to your art and text with
Id in a table of your own.
Select an allyโ
Task<ProjectVResult> SelectAllyAsync(string allyId)
| Parameter | Type | Value |
|---|---|---|
allyId | string | ally.Id of an ally in the current view |
Returns a ProjectVResult:
IsSuccess | Message | What happened |
|---|---|---|
true | empty | The server confirmed the selection, or the ally was already selected. The highlight moves when Render is called |
false | the reason, ready to show | The selection did not happen, or could not be confirmed (see below) |
When it fails โ show Message as it is:
| Situation | What the player sees |
|---|---|
| The server refused the selection | The server's message |
A null or blank allyId | "The request was not confirmed. Please try again." |
| No connection or a timeout | A network message. Do not select again automatically |
SelectAllyAsync does not throw for a bad allyId; it returns a failed result.
One selection at a time. While a selection runs, every other
SelectAllyAsync call gets the first call's result, even for a different
ally, and that other ally is not selected. Wait for the first result before
selecting again.
Level upโ
Task<ProjectVResult> LevelUpAsync(string allyId, IReadOnlyList<ProjectVCharacterXpItemSelection> items)
| Parameter | Type | Value |
|---|---|---|
allyId | string | ally.Id of an ally in the current view |
items | IReadOnlyList<ProjectVCharacterXpItemSelection> | The EXP items to spend, usually ally.XpForNextLevel() or ally.XpForMaximumLevel(). Ignored when the ally has a pending upgrade |
The items are spent in one request and the ally takes every level they cover. EXP short of the next level is kept.
Throws at once, before anything is sent:
| Exception | When |
|---|---|
ArgumentException | allyId is null or blank, or items has a null or invalid entry |
ArgumentNullException | items is null |
Returns a ProjectVResult:
IsSuccess | Message | What happened |
|---|---|---|
true | empty | The server confirmed the level-up. The page redraws by itself |
false | the reason, ready to show | The level-up did not happen, or could not be confirmed (see below) |
When it fails โ show Message as it is. The common reasons:
| Situation | What the player sees |
|---|---|
| Another ally or the character has a level-up that is not confirmed | "Another ally or character has an upgrade pending." |
| No items selected and not enough EXP for the next level | "Select enough EXP for the next level." |
| Not enough currency for the next level | "Not enough currency for the next level." |
| The ally is already at its maximum level | "Maximum level reached." |
| The items are not owned or are not ally EXP items | "The selected EXP items are unavailable for this character kind." |
| The server refused the level-up | The server's message |
| No connection or a timeout | A network message. The level-up may still have gone through (see below) |
When the result is unknown โ for example the connection dropped โ the
level-up stays pending. The ally shows HasPendingUpgrade and the other
allies show IsBlockedByPendingUpgrade. The next LevelUpAsync for that
ally finishes the pending level-up with the same items before anything new.
The pending level-up is shared with the Character screen.
A second tap on the same ally while a level-up runs gets the same result instead of spending twice.
Upgrade the Weaponโ
Task<ProjectVResult> UpgradeWeaponSkillAsync(string allyId)
| Parameter | Type | Value |
|---|---|---|
allyId | string | ally.Id of an ally in the current view |
Show the cost and enable the button from ally.WeaponSkill: Costs for the
price lines and CanUpgrade for the button.
Throws ArgumentException at once when allyId is null or blank.
Returns a ProjectVResult:
IsSuccess | Message | What happened |
|---|---|---|
true | empty | The server confirmed the upgrade. The page redraws by itself |
false | the reason, ready to show | The upgrade did not happen, or could not be confirmed (see below) |
When it fails โ show Message as it is. The common reasons:
| Situation | What the player sees |
|---|---|
| The Weapon cannot be upgraded now | WeaponSkill.BlockedMessage, for example "Maximum level reached." |
| The allies are not loaded yet | "Skill data unavailable." |
| The server refused the upgrade | The server's message |
| No connection or a timeout | A network message. Do not upgrade again automatically |
A second tap while the upgrade runs gets the same result.
Reset the levelโ
Task<ProjectVResult> ResetLevelAsync(string allyId)
| Parameter | Type | Value |
|---|---|---|
allyId | string | ally.Id of an ally in the current view |
The ally goes back to level 1 with 0 EXP, and the EXP items it used come
back. Weapon and skill ranks stay. Show the refund from ally.LevelReset.Refunds
and enable the button from ally.LevelReset.CanReset.
Throws ArgumentException at once when allyId is null or blank.
Returns a ProjectVResult:
IsSuccess | Message | What happened |
|---|---|---|
true | empty | The server confirmed the reset. The page redraws by itself |
false | the reason, ready to show | The reset did not happen, or could not be confirmed (see below) |
When it fails โ show Message as it is. The common reasons:
| Situation | What the player sees |
|---|---|
| The reset is not allowed now | LevelReset.Message, the server's reason |
| The ally is not owned | "This character is not owned." |
| No connection or a timeout | A network message. Do not reset again automatically |
A second tap while the reset runs gets the same result.
Rulesโ
- Commands run one after another. A tap on another command waits until the running one finishes.
- The screen resynchronises by itself. After a result that could not be confirmed, or when the shown data is out of date, the screen reloads the allies in the background. Level-ups, Weapon upgrades and resets wait for that reload. You do not need a Refresh button.
- Refunds come from the server. A reset gives back exactly the EXP items the ally used; EXP gained any other way gives nothing back.
- The server checks every command. The view's
CanUpgrade,CanResetand previews only shape the buttons.
Common mistakesโ
- Selecting a second ally before the first selection finished. The second call returns the first call's result and does not select the second ally.
- Calculating costs, stats or refunds in the Client. Use the view; use local assets only for visuals and animations.
- Removing items or currency yourself after a command. The page updates by itself; your numbers would be wrong.
- Highlighting the selected ally from a local flag. Use
SelectedAllyId. - Passing an empty or null ID to the level, Weapon or reset commands.
They throw; pass
ally.Idfrom the current view. - Using the result after the page closed. Check
this != nullafterawait. - Not disposing the observation. Keep the
IDisposablefromObserveand dispose it inOnDisableorOnDestroy.
Presenterโ
IProjectVAlliancePresenter
is the lower-level interface behind the screen contract. Use it only if you
need what the screen does not give:
| Member | Use it for |
|---|---|
SubscribeAsync(listener) | Every ProjectVAllianceSnapshot: Status, Recovery, RequiresUpgradeSync, and an Error with Code and RequestId |
PendingProgression | The level-up that is not confirmed, shared with Character |
GetUpgradeState(allyId) | Whether the ally can level up now, and why not |
GetXpUpgradePreview(allyId, items) | The level, EXP and stats these items would give |
GetLevelResetPreview(allyId) | Whether a reset is allowed and which EXP items it gives back |
SynchronizeAsync(token) | Reload from the server, for an explicit Sync button |
RetryPendingProgressionAsync(token) | Finish the level-up that is not confirmed |
The presenter commands take a ProjectVCharacterId and a CancellationToken
and return a ProjectVAllianceCommandResult.
Its UpgradeAllyWithXpItemsAsync and UpgradeWeaponSkillAsync throw
ArgumentException when the ally is not owned.
SubscribeAsync listeners may run off the Unity main thread; switch to the
main thread before touching Unity objects. The screen's Observe always
renders on the main thread.