Release Healer Man 0.1.6
This commit is contained in:
1 parent
55cfd43d66
commit
b12fb84859
96 files changed
+10874
-609
No files matched your search
+117
-3
@@ -11,6 +11,11 @@ import {
|
||||
import { TimedEffectStrip } from "./TimedEffectStrip";
|
||||
import { AuraStrip } from "./AuraStrip";
|
||||
import { PLAYER_AGGRO_ID } from "../game/aggro";
|
||||
import { useOnlineGroupStore } from "../app/onlineGroupStore";
|
||||
import type { OnlineGroupMember } from "../app/onlineGroupTypes";
|
||||
import { useOnlineSessionStore } from "../app/onlineSessionStore";
|
||||
import type { OnlineSessionPlayer } from "../app/onlineSessionTypes";
|
||||
import { onlinePlayerActorId } from "../game/onlineSessionRuntime";
|
||||
|
||||
const PARTY_COMMANDS: readonly {
|
||||
command: PartyCommand;
|
||||
@@ -107,6 +112,22 @@ function MemberFrame({ member, selected, onSelect }: {
|
||||
const characterClass = classById(member.classId);
|
||||
const race = raceById(member.raceId);
|
||||
const percent = healthPercent(member.health, member.maxHealth);
|
||||
const absorb = useCombatStore((state) => state.auras.reduce((total, aura) => (
|
||||
aura.targetId === member.id
|
||||
? total + aura.absorbs.reduce((sum, pool) => sum + pool.remaining, 0)
|
||||
: total
|
||||
), 0));
|
||||
const absorbPercent = Math.min(100 - percent, healthPercent(absorb, member.maxHealth));
|
||||
const dispellableCount = useCombatStore((state) => state.auras.reduce((total, aura) => (
|
||||
aura.targetId === member.id && aura.definition.disposition === "debuff" && aura.definition.dispelCategory
|
||||
? total + 1
|
||||
: total
|
||||
), 0));
|
||||
const dispellableNames = useCombatStore((state) => state.auras
|
||||
.filter((aura) => aura.targetId === member.id && aura.definition.disposition === "debuff" && aura.definition.dispelCategory)
|
||||
.map((aura) => aura.definition.name)
|
||||
.join(", "));
|
||||
const resourcePercent = healthPercent(member.resource, member.maxResource);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -118,10 +139,15 @@ function MemberFrame({ member, selected, onSelect }: {
|
||||
<span className="party-frame__sigil" style={{ "--party-class-color": characterClass.color } as CSSProperties}>{characterClass.sigil}</span>
|
||||
<span className="party-frame__body">
|
||||
<span className="party-frame__identity"><strong>{member.name}</strong><small>{PARTY_ROLE_LABELS[member.role]} · {race.name} · {member.specialization}</small></span>
|
||||
{dispellableCount ? <span className="party-frame__dispel" title={dispellableNames}>DISPEL {dispellableCount}</span> : null}
|
||||
<span className="party-frame__health" role="progressbar" aria-label={`${member.name} health`} aria-valuemin={0} aria-valuemax={member.maxHealth} aria-valuenow={Math.max(0, member.health)}>
|
||||
<i style={{ width: `${percent}%` }} />
|
||||
{absorb > 0 ? <em style={{ left: `${percent}%`, width: `${absorbPercent}%` }} /> : null}
|
||||
<b>{Math.ceil(member.health)} / {member.maxHealth}</b>
|
||||
</span>
|
||||
<span className={`party-frame__resource party-frame__resource--${member.resourceType}`} title={`${Math.floor(member.resource)} / ${member.maxResource} ${member.resourceName}`}>
|
||||
<i style={{ width: `${resourcePercent}%` }} />
|
||||
</span>
|
||||
<TimedEffectStrip targetId={member.id} className="timed-effect-strip--party" />
|
||||
<AuraStrip targetId={member.id} className="aura-strip--party" />
|
||||
</span>
|
||||
@@ -129,19 +155,85 @@ function MemberFrame({ member, selected, onSelect }: {
|
||||
);
|
||||
}
|
||||
|
||||
function OnlineMemberFrame({
|
||||
member,
|
||||
player,
|
||||
selected,
|
||||
onSelect,
|
||||
}: {
|
||||
member: OnlineGroupMember;
|
||||
player: OnlineSessionPlayer | null;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const character = member.character;
|
||||
if (!character) return null;
|
||||
const characterClass = classById(character.classId as Parameters<typeof classById>[0]);
|
||||
const percent = player ? healthPercent(player.health, player.maxHealth) : 0;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`party-frame party-frame--online ${selected ? "is-selected" : ""} ${player && player.health <= 0 ? "is-down" : ""}`}
|
||||
aria-label={`Target ${character.name}, online player`}
|
||||
aria-pressed={selected}
|
||||
disabled={!player}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<span className="party-frame__sigil" style={{ "--party-class-color": characterClass.color } as CSSProperties}>{characterClass.sigil}</span>
|
||||
<span className="party-frame__body">
|
||||
<span className="party-frame__identity"><strong>{character.name}</strong><small>{member.role ? PARTY_ROLE_LABELS[member.role] : "Player"} · {member.username}</small></span>
|
||||
{player ? (
|
||||
<>
|
||||
<span className="party-frame__health" role="progressbar" aria-label={`${character.name} health`} aria-valuemin={0} aria-valuemax={player.maxHealth} aria-valuenow={Math.max(0, player.health)}>
|
||||
<i style={{ width: `${percent}%` }} />
|
||||
<b>{Math.ceil(player.health)} / {player.maxHealth}</b>
|
||||
</span>
|
||||
<span className="party-frame__resource" title={`${Math.floor(player.resource)} / ${player.maxResource} ${player.resourceName}`}>
|
||||
<i style={{ width: `${healthPercent(player.resource, player.maxResource)}%` }} />
|
||||
</span>
|
||||
<span className="party-frame__online-status"><i /> Live</span>
|
||||
</>
|
||||
) : <span className="party-frame__online-status">Connecting…</span>}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/** Compact roster for the player plus the selected number of targetable AI allies. */
|
||||
export function PartyFrames() {
|
||||
const character = useShellStore((state) => state.activeCharacter);
|
||||
const playerRole = useShellStore((state) => state.activeDungeonRole);
|
||||
const playerHealth = useCombatStore((state) => state.health);
|
||||
const playerMaxHealth = useCombatStore((state) => state.maxHealth);
|
||||
const playerResource = useCombatStore((state) => state.resource);
|
||||
const playerMaxResource = useCombatStore((state) => state.maxResource);
|
||||
const playerResourceName = useCombatStore((state) => state.resourceName);
|
||||
const playerAuraAbsorb = useCombatStore((state) => state.auras.reduce((total, aura) => (
|
||||
aura.targetId === PLAYER_AGGRO_ID
|
||||
? total + aura.absorbs.reduce((sum, pool) => sum + pool.remaining, 0)
|
||||
: total
|
||||
), 0));
|
||||
const playerShield = useCombatStore((state) => state.shield);
|
||||
const members = usePartyStore((state) => state.members);
|
||||
const selectedMemberId = usePartyStore((state) => state.selectedMemberId);
|
||||
const selfSelected = usePartyStore((state) => state.selfSelected);
|
||||
const routeBlocked = usePartyStore((state) => state.routeBlocked);
|
||||
const sessionOwnerId = useShellStore((state) => state.session?.ownerId ?? null);
|
||||
const onlineGroup = useOnlineGroupStore((state) => state.snapshot.group);
|
||||
const onlineActivity = useOnlineGroupStore((state) => state.activeActivity);
|
||||
const onlineSession = useOnlineSessionStore((state) => state.snapshot);
|
||||
const selectedOnlineActorId = useCombatStore((state) => state.selectedFriendlyActorId);
|
||||
const playerPercent = healthPercent(playerHealth, playerMaxHealth);
|
||||
if (!members.length) return null;
|
||||
const groupSize = members.length + 1;
|
||||
const playerAbsorb = playerShield + playerAuraAbsorb;
|
||||
const playerAbsorbPercent = Math.min(100 - playerPercent, healthPercent(playerAbsorb, playerMaxHealth));
|
||||
const onlineMembers = onlineActivity && onlineGroup
|
||||
? onlineGroup.members.filter((member) => member.accountId !== sessionOwnerId && member.character)
|
||||
: [];
|
||||
const onlinePlayersByAccount = new Map(
|
||||
(onlineSession?.players ?? []).map((player) => [player.accountId, player]),
|
||||
);
|
||||
if (!members.length && !onlineMembers.length) return null;
|
||||
const groupSize = members.length + onlineMembers.length + 1;
|
||||
|
||||
const selectMember = (id: string) => {
|
||||
useCombatStore.getState().clearTarget();
|
||||
@@ -153,10 +245,16 @@ export function PartyFrames() {
|
||||
usePartyStore.getState().selectSelf();
|
||||
};
|
||||
|
||||
const selectOnlineMember = (accountId: string) => {
|
||||
useCombatStore.getState().clearTarget();
|
||||
usePartyStore.getState().clearSelection();
|
||||
useCombatStore.getState().selectFriendlyActor(onlinePlayerActorId(accountId));
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="party-frames" aria-label={`${groupSize} player party`}>
|
||||
<header><span>Party</span><b className={routeBlocked ? "is-warning" : ""}>{routeBlocked ? "Route blocked" : `${groupSize} / ${groupSize}`}</b></header>
|
||||
<PartyCommandBar />
|
||||
{members.length ? <PartyCommandBar /> : null}
|
||||
<button
|
||||
type="button"
|
||||
className={`party-frame party-frame--player ${selfSelected ? "is-selected" : ""} ${playerHealth <= 0 ? "is-down" : ""}`}
|
||||
@@ -169,12 +267,28 @@ export function PartyFrames() {
|
||||
<span className="party-frame__identity"><strong>{character?.name ?? "Adventurer"}</strong><small>You · {PARTY_ROLE_LABELS[playerRole]}</small></span>
|
||||
<span className="party-frame__health" role="progressbar" aria-label="Player health" aria-valuemin={0} aria-valuemax={playerMaxHealth} aria-valuenow={Math.max(0, playerHealth)}>
|
||||
<i style={{ width: `${playerPercent}%` }} />
|
||||
{playerAbsorb > 0 ? <em style={{ left: `${playerPercent}%`, width: `${playerAbsorbPercent}%` }} /> : null}
|
||||
<b>{Math.ceil(playerHealth)} / {playerMaxHealth}</b>
|
||||
</span>
|
||||
<span className="party-frame__resource" title={`${Math.floor(playerResource)} / ${playerMaxResource} ${playerResourceName}`}>
|
||||
<i style={{ width: `${healthPercent(playerResource, playerMaxResource)}%` }} />
|
||||
</span>
|
||||
<TimedEffectStrip targetId={null} className="timed-effect-strip--party" />
|
||||
<AuraStrip targetId={PLAYER_AGGRO_ID} className="aura-strip--party" />
|
||||
</span>
|
||||
</button>
|
||||
{onlineMembers.map((member) => {
|
||||
const actorId = onlinePlayerActorId(member.accountId);
|
||||
return (
|
||||
<OnlineMemberFrame
|
||||
key={member.accountId}
|
||||
member={member}
|
||||
player={onlinePlayersByAccount.get(member.accountId) ?? null}
|
||||
selected={selectedOnlineActorId === actorId}
|
||||
onSelect={() => selectOnlineMember(member.accountId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{members.map((member) => (
|
||||
<MemberFrame
|
||||
key={member.id}
|
||||
|
||||
Reference in new issue
Block a user