Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
70ffaea
chore(mobile): target Sepolia for the EVM surface
mobuild4u Aug 5, 2026
0b5d937
feat(mobile): add the v2 chain constants
mobuild4u Aug 5, 2026
99151bb
feat(mobile): add the v2 ABIs and contract surface
mobuild4u Aug 5, 2026
ed5229e
refactor(mobile): swap petsContractParams to PetsEvmConfig
mobuild4u Aug 5, 2026
7f23ab5
feat(mobile): read pets through the v2 adapter layer
mobuild4u Aug 5, 2026
86a2f65
feat(mobile): add a toast provider and the error hooks bound to it
mobuild4u Aug 5, 2026
80aad2b
feat(mobile): add the navigation shell
mobuild4u Aug 5, 2026
ee7f9b4
feat(mobile): mount the navigator behind a connect gate
mobuild4u Aug 5, 2026
a1a3578
feat(mobile): build the gallery screen on the composite hook
mobuild4u Aug 5, 2026
69ea76f
feat(mobile): add the defense consent screen
mobuild4u Aug 5, 2026
4db134c
feat(mobile): add the breeding screen
mobuild4u Aug 5, 2026
e5b2e65
feat(mobile): add the marriage screen
mobuild4u Aug 6, 2026
9291eb3
feat(mobile): add the battle screen
mobuild4u Aug 6, 2026
3473a0a
feat(mobile): gate play on the approved WalletConnect chain
mobuild4u Aug 6, 2026
460bc5e
feat(mobile): restore backend sign-in from the tab shell
mobuild4u Aug 6, 2026
b2d226c
cleanup(mobile): retire the last of the v1 contract surface
mobuild4u Aug 6, 2026
b86d937
feat(mobile): register the Solana signer so the adapter is reachable
mobuild4u Aug 6, 2026
6356643
fix(mobile): persist the player's battle-commitment evidence
mobuild4u Aug 6, 2026
fd4047c
docs(mobile): rewrite the README for this monorepo
mobuild4u Aug 6, 2026
e8f0b73
test(mobile): cover usePetGallery, and stop a failed refresh rejecting
mobuild4u Aug 6, 2026
b98ab6e
test(mobile): cover the mint sheet and the landing wallet panel
mobuild4u Aug 6, 2026
0038089
refactor(mobile): resolve contract addresses per chain
mobuild4u Aug 6, 2026
c09f0b9
feat(mobile): target the Base Sepolia deployment
mobuild4u Aug 6, 2026
785aa51
fix(mobile): stop offering a chain with no contracts
mobuild4u Aug 6, 2026
e34b784
fix(mobile): link the battle to the room it minted
mobuild4u Aug 6, 2026
b476de0
fix(mobile): name a spouse the roster does not hold
mobuild4u Aug 6, 2026
b59ea8a
feat(mobile): take battle updates by push, not just polling
mobuild4u Aug 6, 2026
7e67ecc
feat(mobile): send a pet to another wallet
mobuild4u Aug 6, 2026
a0d4481
test(mobile): make Solana testable and fix cold-cache runs
mobuild4u Aug 6, 2026
127df58
fix(mobile): stop the mint sheet looping itself to death
mobuild4u Aug 8, 2026
7c32b9f
fix(mobile): stop telling petless players their pets are resting
mobuild4u Aug 9, 2026
28b3cb1
Merge branch 'main' into feat/mobile-frontend-parity
mobuild4u Aug 9, 2026
653565e
fix(mobile): offer a network fix that a frozen session can accept
mobuild4u Aug 9, 2026
845193a
Merge branch 'main' into feat/mobile-frontend-parity
mobuild4u Aug 10, 2026
7a9cae0
Merge branch 'main' into feat/mobile-frontend-parity
mobuild4u Aug 11, 2026
22decfd
fix(shared): read entropy and settle events from the request block
mobuild4u Aug 11, 2026
5cfa6f3
feat(mobile): add the leaderboard screen
mobuild4u Aug 11, 2026
5c83b14
feat(mobile): find a marriage partner by search instead of by id
mobuild4u Aug 11, 2026
8466bd6
feat(mobile): add inventory and equipment
mobuild4u Aug 11, 2026
5d8cd11
feat(mobile): replay a battle strike by strike
mobuild4u Aug 11, 2026
9eaf2bc
feat(mobile): play the AI result dialogue after a battle
mobuild4u Aug 11, 2026
e94f6df
feat(mobile): add private chat
mobuild4u Aug 11, 2026
c82abe7
feat(mobile): report defence consent, stud fees and metadata sync
mobuild4u Aug 12, 2026
4279551
fix(mobile): give marriage an entry point, and test that every route …
mobuild4u Aug 12, 2026
7415adc
fix(mobile): say why the opponent list is empty, and repair file enco…
mobuild4u Aug 12, 2026
83631a8
fix(mobile): put Allow Challenges where it can be found
mobuild4u Aug 12, 2026
969af9b
fix(mobile): remove the push animation for account-sheet screens
mobuild4u Aug 12, 2026
dbc1fea
feat(mobile): show what the pet card already knew
mobuild4u Aug 12, 2026
bf3ef19
feat(mobile): draw pet art wherever a pet is listed
mobuild4u Aug 12, 2026
0f0e58d
feat(mobile): show a pet's equipped gear on its card
mobuild4u Aug 12, 2026
c51d7be
fix(mobile): select an opponent by owner and id, not id alone
mobuild4u Aug 12, 2026
0ea0a27
feat(mobile): draw item art in the bag and the equip screen
mobuild4u Aug 12, 2026
5d1d338
fix(mobile): tell a signed-out player to sign in
mobuild4u Aug 12, 2026
80ce844
fix(mobile): follow the pet a card sends, not just the first one
mobuild4u Aug 12, 2026
6bc46f7
feat(mobile): ask for a session instead of claiming there is no data
mobuild4u Aug 12, 2026
afaf385
feat(mobile): approve a battle signing session
mobuild4u Aug 12, 2026
b5fd372
feat(mobile): name what a battle is waiting on
mobuild4u Aug 12, 2026
0615313
fix(mobile): add breed settle and cancel
mobuild4u Aug 13, 2026
c943999
fix(shared): treat a published battle as settled
mobuild4u Aug 13, 2026
0dfcbcc
fix(mobile): report the battle session outcome
mobuild4u Aug 13, 2026
d120e82
fix(shared): keep pet ids aligned with pet reads
mobuild4u Aug 13, 2026
b6bedc4
feat(mobile): follow the wallet's network
mobuild4u Aug 13, 2026
6a65721
fix(mobile): say which pet failed to load
mobuild4u Aug 13, 2026
73502b0
fix(mobile): report a sign-in that never reached the wallet
mobuild4u Aug 13, 2026
b87602e
fix(mobile): quiet WalletConnect log noise
mobuild4u Aug 13, 2026
b2011e7
chore: repair the malformed pnpm lockfile
mobuild4u Aug 13, 2026
9115062
feat(mobile): show when a proposal expires
mobuild4u Aug 13, 2026
2c8e035
fix(mobile): keep incoming proposals up to date
mobuild4u Aug 13, 2026
9ec5aa6
feat(mobile): read the chain through your own RPC
mobuild4u Aug 13, 2026
8c988e2
feat(mobile): show why a battle was refused
mobuild4u Aug 13, 2026
e62e920
fix(mobile): silence the session_request noise
mobuild4u Aug 14, 2026
bfed001
fix(mobile): stop two wallet prompts at once
mobuild4u Aug 14, 2026
c3ff4ac
fix(shared): stop racing the settle keeper
mobuild4u Aug 14, 2026
9a726c7
feat(mobile): pin the action bar to the bottom
mobuild4u Aug 15, 2026
042b96d
feat(mobile): animate the account sheet in and out
mobuild4u Aug 15, 2026
6c082c1
feat(mobile): swipe through pets one per page
mobuild4u Aug 15, 2026
a573315
feat(mobile): swipe through active marriages
mobuild4u Aug 15, 2026
1f3f6c3
feat(mobile): move navigation into a drawer
mobuild4u Aug 15, 2026
afabe3d
feat(mobile): raise the account sheet from the bottom
mobuild4u Aug 15, 2026
d4c51c0
refactor(mobile): give the warning colour a name
mobuild4u Aug 15, 2026
589db1b
feat(mobile): swipe from the edge to open the menu
mobuild4u Aug 15, 2026
a3cb66f
feat(mobile): hold a pet chip to see its card
mobuild4u Aug 15, 2026
b41de3a
feat(mobile): mark the breeding pair with a heart
mobuild4u Aug 15, 2026
f499f75
feat(mobile): mark the two sides of breed and battle
mobuild4u Aug 15, 2026
c83066a
feat(mobile): show the picked pet's stats inline
mobuild4u Aug 15, 2026
d1dc345
feat(mobile): show opponents as a scrolling strip
mobuild4u Aug 15, 2026
15d9b41
refactor(mobile): drop the win estimate from battle
mobuild4u Aug 15, 2026
8688d3d
refactor(mobile): split watching a battle from arranging one
mobuild4u Aug 15, 2026
9db5c19
feat(mobile): open an arena when the battle starts
mobuild4u Aug 15, 2026
271ce8c
feat(mobile): announce the fight before the arena
mobuild4u Aug 15, 2026
7a58cf3
feat(mobile): put each battle line beside who said it
mobuild4u Aug 16, 2026
f6ffdcc
refactor(mobile): share one text walk across the suites
mobuild4u Aug 16, 2026
1c55048
refactor(mobile): share the pet stat derivations
mobuild4u Aug 16, 2026
d5a412c
cleanup(mobile): drop the unused svg dependency
mobuild4u Aug 16, 2026
3bcf592
refactor(mobile): give leaderboard and chat controllers
mobuild4u Aug 16, 2026
d0a58f3
fix(mobile): name the toast file like a component
mobuild4u Aug 16, 2026
1752529
fix(mobile): stop AppKit's unawaited connect crashing boot
mobuild4u Aug 14, 2026
fe07cf5
fix(mobile): show the chain's reason when marriage fails
mobuild4u Aug 16, 2026
d6c3622
feat(mobile): label controls for screen readers
mobuild4u Aug 16, 2026
bfd5c99
test: fix room socket subscription test timing
mobuild4u Aug 16, 2026
0da6ada
test(mobile): stop the replay test racing the clock
mobuild4u Aug 16, 2026
026c365
fix(mobile): stop a test depending on a gitignored .env
mobuild4u Aug 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions docs/plan-battle-stage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# Plan: the battle stage

`BattleStage` is the full-screen arena that opens when Start is pressed. It currently renders
a spinner, a flat list of taunt lines, an HP-bar scene, a scrolling strike log, and a result
sheet centred over all of it. It reads as a status page for a fight rather than the fight.

Scope is `mobile/`. Three of the five items below need no other package. The two that touch
data are fixed in `useBattlePanel`, which is mobile's own file, not in `shared/`.

---

## Three findings up front, because they decide the order

**1. The panel throws away who is speaking.** `UseBattlePanel` exposes `taunts: string[]`,
built as `taunts.turns?.map((t) => t.text)`. The underlying `DialogueTurn` is
`{ speaker: 'attacker' | 'defender'; phase; text }`, so the side each line belongs to exists
and is discarded one layer before the component that needs it. Nothing can put a line beside
its avatar until this stops. This is the whole of the complaint about message history, and it
is a two-line change.

**2. The panel throws away the shape of each strike, too.** `strikeLog: string[]` is built by
mapping `animation.history` through a formatter. `history` is `StrikeLogEntry[]`, which
carries `attacker: 1 | 2`, `damage`, `crit`, `isMagic`, `heal`, `elementMult`,
`furyTriggered`. No hit effect can know who was hit, how hard, or whether it crit while the
component only receives a sentence.

**3. The two pets stop existing before the fight ends.** Publishing a receipt puts the fighter
on cooldown, which drops it out of `readyPets`, so `panel.fighter` is null by the time a
result is on screen, and `panel.opponent` can leave the matchmaking list the same way. The
panel already solves this for names with `personasRef`, a `BattlePersonas` captured at start.
`DialoguePetInput` holds `petId`, `name`, `level`, `rarity` and `dna`, which is enough to draw
an avatar. **Any avatar in the arena must read personas, not `fighter`/`opponent`, or it
vanishes at the moment of victory.**

---

## 1. Two fighters, facing each other

### Current state

`BattleScene` renders two stacked `HpBar` rows, a flourish line and a scrolling log. There is
no pet on screen at any point. The one thing a player recognises a pet by, its art, is absent
from the only screen that is about two specific pets.

### Proposed

An arena band across the top: attacker on the left, defender on the right, each with

- `PetArt` at 72, from the persona's `petId` and `dna`
- name and level under it
- an HP bar under that, keeping `HpBar`'s existing drain animation
- the loser dimmed to ~40% opacity once a result lands

Between them, the `GlyphDivider` already used by Breed and Battle setup, with `VS`.

### Work

| file | change |
|---|---|
| `src/screens/parts/BattleStage.tsx` | the arena band |
| `src/screens/parts/BattleScene.tsx` | HP bars move into the band; the component keeps the log |
| `src/hooks/battle/useBattlePanel.ts` | expose `personas` so the arena can draw both sides |

`PetArt` takes a `Pet`. A persona is not one, so either it gets a narrow prop type
(`Pick<Pet, 'id' | 'chain' | 'assetKey' | 'dna' | 'name'>`, which is what it already reads) or
the panel exposes the two pets it captured rather than only their dialogue inputs. The second
is less work and keeps `PetArt` untouched.

---

## 2. A line sits beside whoever said it

### Current state

```tsx
{panel.taunts.map((line, i) => (
<Text key={i} style={styles.tauntLine}>{line}</Text>
))}
```

Every line, both speakers, one column, no attribution. The result dialogue does better: it
renders `resultTurns` with the speaker's name in the speaker's colour. The taunts cannot,
because the speaker is gone by then (finding 1).

### Proposed

Taunts and result turns become the same thing: a speech bubble anchored to its speaker.

- attacker lines left-aligned, tail pointing left, cyan border
- defender lines right-aligned, tail pointing right, magenta border
- max width ~78%, so a bubble never spans the arena and the side is obvious at a glance
- newest last, the column scrolls, and it auto-scrolls only when already at the bottom
- each bubble fades and rises 8px on arrival

The tail is a rotated 8px square behind the bubble, not an image. There is no icon set here
and this needs no dependency.

### Work

| file | change |
|---|---|
| `src/hooks/battle/useBattlePanel.ts` | `taunts: DialogueTurn[]` instead of `string[]` |
| `src/screens/parts/SpeechBubble.tsx` *(new)* | one bubble, side and colour from the speaker |
| `src/screens/parts/BattleStage.tsx` | one column, taunts then result turns, in order |

Two consumers change with the type: `BattleStage` and any test asserting on `panel.taunts`.
`useResultDialogue` already returns `DialogueTurn[]`, so the two sources become one list.

---

## 3. Strikes should be visible, not narrated

### Current state

A strike changes two numbers and appends a sentence. `useLiveBattleAnimation` already paces
them one every 700ms, so the timing is there and nothing uses it.

### Proposed

Per strike, driven off `StrikeLogEntry`:

- the striker lunges 12px toward its target and back, ~180ms
- the struck pet flashes its border and shakes 6px, ~120ms
- a damage number floats up 24px from the struck pet and fades, in `danger`, doubled in size
and in `warning` when `crit`
- `isMagic` tints the number `purple` rather than `danger`
- `heal > 0` floats a second number up from the striker in `success`
- `furyTriggered` pulses the striker's glow

All of it is transform and opacity, so `useNativeDriver: true` throughout. The HP bar stays on
the JS driver because width is a layout property, which is already the case and already
commented.

### Work

| file | change |
|---|---|
| `src/hooks/battle/useBattlePanel.ts` | expose `currentStrike: StrikeLogEntry \| null` beside `flourish` |
| `src/screens/parts/StrikeEffects.tsx` *(new)* | the lunge, shake and floating numbers |
| `src/screens/parts/BattleScene.tsx` | keep the log, hand the arena the strike |

Keep `flourish`. It is the line a player reads when they look away and back, and it is what
the screen reader gets.

---

## 4. The verdict should not cover the fight

### Current state

An absolute overlay centred on the arena, holding title, rounds, HP left, XP, the whole result
dialogue and two buttons. It lands on top of the two pets the player was just watching, which
is the moment they most want to see.

### Proposed

- the verdict is a banner in the arena band, between the two pets, where `VS` was: `Victory`
or `Defeat`, plus `rounds · HP left · +XP` on one line
- the loser dims, the winner keeps its glow and pulses once
- the result dialogue joins the same bubble column as everything else, rather than being a
second list inside a sheet
- `Watch again` and `Close` move to a pinned row at the bottom, matching `ScreenActionBar`
everywhere else in the app
- the banner drops in and settles, ~220ms spring, and holds until dismissed

This removes the nested-overlay layout entirely. The arena is one screen with a beginning, a
middle and an end, rather than a screen with a sheet on top of it.

### Work

| file | change |
|---|---|
| `src/screens/parts/BattleStage.tsx` | banner replaces the overlay; actions move to a pinned row |

---

## 5. Reduced motion, and what the screen reader gets

Everything above is decoration over information that must survive without it.

- `useReduceMotion` already exists. Under it: no lunge, no shake, no float, no banner spring.
The damage number appears and disappears rather than travelling, and HP bars jump.
- the arena band gets an `accessibilityLabel` naming both fighters and their HP
- bubbles are read as "Rex says: …", which the speaker makes possible for the first time
- the verdict banner takes `accessibilityLiveRegion="polite"` so it is announced when it lands
- `GlyphDivider` is already hidden from screen readers and stays that way

---

## Decisions needed

| # | question | recommendation |
|---|---|---|
| 1 | Persona-shaped avatars, or expose the captured `Pet` objects from the panel? | **Expose the pets.** `PetArt` is untouched and the arena gets rarity and art for free |
| 2 | Do taunts and result lines share one column, or stay two lists? | **One column.** They are the same conversation, and one list is what makes the speaker rule uniform |
| 3 | Damage numbers on every strike, or only crits? | **Every strike.** A fight is 4 to 12 strikes, and only-on-crit reads as a bug the rest of the time |

---

## Testing

`BattleScreen.test.tsx` is 47 tests and already drives the arena through `openArena`. Four of
them assert on `panel.taunts` as strings and will need the turn shape.

Worth adding:

- a taunt from the defender renders on the defender's side, and one from the attacker does not
- the arena still names both fighters after the fighter leaves `readyPets`, which is finding 3
and the one regression that would otherwise ship silently
- a crit renders a different damage treatment from a normal strike
- the verdict does not cover the two pets: both avatars are still mounted with the banner up
- under reduced motion, a strike still updates HP and still shows its damage number

The existing replay tests already advance timers one strike at a time. Strike effects should
reuse that helper rather than introducing a second timing model.

---

## Suggested order

1. **§2 speech bubbles.** The complaint that started this, and the smallest change: a type on
the panel plus one new component.
2. **§1 the arena band.** Everything else hangs off having two pets on screen.
3. **§4 the verdict.** Once the band exists, the banner has somewhere to live, and this
deletes the overlay rather than adding to it.
4. **§3 strike effects.** The largest, and the only one that is pure decoration, so it goes
last and can be cut without leaving a gap.
5. **§5 reduced motion.** Alongside §3, not after it: retrofitting a motion flag across four
animations costs more than passing it in as each one lands.
4 changes: 4 additions & 0 deletions mobile/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
module.exports = {
root: true,
extends: '@react-native',
// Generated by `jest --coverage`. Already gitignored, but eslint has no view of
// that and its bundled report scripts trip the eslint-comments rules, so a
// coverage run would otherwise turn a clean lint into four warnings.
ignorePatterns: ['coverage/'],
};
78 changes: 62 additions & 16 deletions mobile/App.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,75 @@
import React from 'react';
import "@walletconnect/react-native-compat";
import { AppKitProvider } from '@reown/appkit-react-native';
import { StatusBar } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer } from '@react-navigation/native';
import { AppKit, AppKitProvider } from '@reown/appkit-react-native';
import { WagmiProvider } from 'wagmi';
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient, ApiClientProvider, AuthProvider } from '@shared/core';
import { queryClient, ApiClientProvider, AuthProvider, PetsConfigProvider } from '@shared/core';

import { appKit, wagmiConfig } from './src/AppKitConfig';
import AppRoot from './src/AppContent.tsx';
import { API_URL } from './config';
import { useEvmPetsConfig } from './src/petsContractParams';
import { ToastProvider } from './src/components/ui/Toast';
import SignInErrorReporter from './src/components/SignInErrorReporter';
import { RootNavigator } from './src/navigation/RootNavigator';
import { neon } from './src/theme/neon';
import { SolanaAppKitAnchorBridge } from './src/solana/SolanaAppKitAnchorBridge';
import { SolanaAuthSigner } from './src/solana/SolanaAuthSigner';

/**
* Provider order matches `frontend/src/AppProviders.tsx`, with `NavigationContainer`
* where its `BrowserRouter` sits.
*
* `SafeAreaProvider` is outermost so `ToastProvider` can measure a real bottom
* inset rather than assuming one. `AppKit` renders as a sibling of the navigator
* so its connect sheet is reachable from the landing screen and the tab shell
* alike.
*
* `SolanaAuthSigner` is a non-wrapping sibling inside `AppKitProvider`, matching
* where `AppProviders.tsx` puts its own: it registers rather than provides, and
* it has to sit above `AuthProvider`, which reads what it registers.
*/

/**
* Supplies the contract config for whichever deployment chain the wallet is on.
*
* A component rather than a constant because the config now depends on
* `useAccount`, and hooks only run inside the tree. It has to sit under
* `WagmiProvider`, which it does.
*/
function PetsConfig({ children }: { children: React.ReactNode }) {
return <PetsConfigProvider evm={useEvmPetsConfig()}>{children}</PetsConfigProvider>;
}

export default function App() {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<AppKitProvider instance={appKit}>
<SolanaAppKitAnchorBridge>
<ApiClientProvider baseURL={API_URL}>
<AuthProvider>
<AppRoot />
</AuthProvider>
</ApiClientProvider>
</SolanaAppKitAnchorBridge>
</AppKitProvider>
</QueryClientProvider>
</WagmiProvider>
<SafeAreaProvider>
<StatusBar barStyle="light-content" backgroundColor={neon.bgDeep} />
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<AppKitProvider instance={appKit}>
<SolanaAuthSigner />
<SolanaAppKitAnchorBridge>
<ApiClientProvider baseURL={API_URL}>
<AuthProvider>
<PetsConfig>
<ToastProvider>
{/* Inside ToastProvider and under AuthProvider, which is what it needs. */}
<SignInErrorReporter />
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
<AppKit />
</ToastProvider>
</PetsConfig>
</AuthProvider>
</ApiClientProvider>
</SolanaAppKitAnchorBridge>
</AppKitProvider>
</QueryClientProvider>
</WagmiProvider>
</SafeAreaProvider>
);
}
Loading
Loading