Skip to main content

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 useIt gives you
IProjectVAllianceScreenThe screen contract: observe the allies, select, level up, upgrade, reset
ProjectVAllianceViewWhat to draw: loading state, allies, the selected ally, EXP items
ProjectVAllyViewOne ally: level, EXP, stats, Weapon skill, previews
ProjectVXpItemViewOne EXP item and how many the player owns
ProjectVCharacterXpItemSelectionThe EXP items and quantities to spend
ProjectVResultThe 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:

MemberTypeUse it for
IsLoadingboolTrue until the allies arrive the first time. Show a spinner
LoadErrorstring?Set when that first load failed. Show it with a Retry button that closes and reopens the page
AlliesIReadOnlyList<ProjectVAllyView>The allies the player owns
SelectedAllyIdstring?The ally the player takes into battle; null when none is selected. Highlight from this, not from a local flag
XpItemsIReadOnlyList<ProjectVXpItemView>EXP items: ItemId, Experience (EXP per item), Owned
PendingUpgradeAllyIdstring?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 showRead
Level and EXPLevel, Experience; Upgrade for what the next level needs
StatsStats (health, attack, armor and so on), as on Character
Max levelIsMaxLevel
The "+1" and "Max" selectionsXpForNextLevel(), XpForMaximumLevel(); empty when no level can be reached
What a selection would doPreviewXp(items): CanSubmit, Message, GrantedExperience, TargetLevel
The Weapon upgradeWeaponSkill: Level, MaxLevel, Costs, CanUpgrade, BlockedMessage
The reset refundLevelReset: CanReset, Message, Refunds
"Finishing upgrade"HasPendingUpgrade: this ally's level-up is not confirmed yet
Level buttons disabledIsBlockedByPendingUpgrade: 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)
ParameterTypeValue
allyIdstringally.Id of an ally in the current view

Returns a ProjectVResult:

IsSuccessMessageWhat happened
trueemptyThe server confirmed the selection, or the ally was already selected. The highlight moves when Render is called
falsethe reason, ready to showThe selection did not happen, or could not be confirmed (see below)

When it fails โ€” show Message as it is:

SituationWhat the player sees
The server refused the selectionThe server's message
A null or blank allyId"The request was not confirmed. Please try again."
No connection or a timeoutA 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)
ParameterTypeValue
allyIdstringally.Id of an ally in the current view
itemsIReadOnlyList<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:

ExceptionWhen
ArgumentExceptionallyId is null or blank, or items has a null or invalid entry
ArgumentNullExceptionitems is null

Returns a ProjectVResult:

IsSuccessMessageWhat happened
trueemptyThe server confirmed the level-up. The page redraws by itself
falsethe reason, ready to showThe level-up did not happen, or could not be confirmed (see below)

When it fails โ€” show Message as it is. The common reasons:

SituationWhat 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-upThe server's message
No connection or a timeoutA 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)
ParameterTypeValue
allyIdstringally.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:

IsSuccessMessageWhat happened
trueemptyThe server confirmed the upgrade. The page redraws by itself
falsethe reason, ready to showThe upgrade did not happen, or could not be confirmed (see below)

When it fails โ€” show Message as it is. The common reasons:

SituationWhat the player sees
The Weapon cannot be upgraded nowWeaponSkill.BlockedMessage, for example "Maximum level reached."
The allies are not loaded yet"Skill data unavailable."
The server refused the upgradeThe server's message
No connection or a timeoutA 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)
ParameterTypeValue
allyIdstringally.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:

IsSuccessMessageWhat happened
trueemptyThe server confirmed the reset. The page redraws by itself
falsethe reason, ready to showThe reset did not happen, or could not be confirmed (see below)

When it fails โ€” show Message as it is. The common reasons:

SituationWhat the player sees
The reset is not allowed nowLevelReset.Message, the server's reason
The ally is not owned"This character is not owned."
No connection or a timeoutA 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, CanReset and 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.Id from the current view.
  • Using the result after the page closed. Check this != null after await.
  • Not disposing the observation. Keep the IDisposable from Observe and dispose it in OnDisable or OnDestroy.

Presenterโ€‹

IProjectVAlliancePresenter is the lower-level interface behind the screen contract. Use it only if you need what the screen does not give:

MemberUse it for
SubscribeAsync(listener)Every ProjectVAllianceSnapshot: Status, Recovery, RequiresUpgradeSync, and an Error with Code and RequestId
PendingProgressionThe 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.