feat(presentation): project immutable public page models
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { selectRoomPlayers, selectSelfPlayer } from '../platform/stores/selectors.ts';
|
||||
import type { PlatformPlayer, PlatformState } from '../platform/stores/platform-types.ts';
|
||||
import type { PageId, PageModel, PlayerModel, SeatModel } from './ui-contracts.ts';
|
||||
|
||||
const playerModel = (player: PlatformPlayer): PlayerModel => Object.freeze({
|
||||
playerid: player.playerid,
|
||||
nickname: player.nickname,
|
||||
avatar: player.avatar,
|
||||
bean: player.bean,
|
||||
});
|
||||
|
||||
function requireSelf(state: PlatformState): PlatformPlayer {
|
||||
const self = selectSelfPlayer(state);
|
||||
if (self === null) {
|
||||
throw new Error('players.selfPlayerId: expected authenticated player');
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
function selectRoomModel(state: PlatformState): Extract<PageModel, { page: 'room' }> {
|
||||
if (state.room.kind !== 'inside') {
|
||||
throw new Error('room: expected player to be inside a room');
|
||||
}
|
||||
const room = state.room;
|
||||
const self = requireSelf(state);
|
||||
if (room.seatPlayerIds[room.selfSeat] !== self.playerid) {
|
||||
throw new RangeError('room.selfSeat: expected authenticated player seat');
|
||||
}
|
||||
|
||||
const seats: SeatModel[] = [];
|
||||
const roomPlayers = selectRoomPlayers(state);
|
||||
for (let seat = 0; seat < roomPlayers.length; seat += 1) {
|
||||
const player = roomPlayers[seat];
|
||||
if (player === null) {
|
||||
seats.push(Object.freeze({ kind: 'empty', seat }));
|
||||
continue;
|
||||
}
|
||||
if (player.onstate === undefined) {
|
||||
throw new TypeError(
|
||||
`room.seatPlayerIds[${seat}] -> players.entities[${player.playerid}].onstate: expected protocol number`,
|
||||
);
|
||||
}
|
||||
if (player.isprepare === undefined) {
|
||||
throw new TypeError(
|
||||
`room.seatPlayerIds[${seat}] -> players.entities[${player.playerid}].isprepare: expected protocol number`,
|
||||
);
|
||||
}
|
||||
seats.push(Object.freeze({
|
||||
kind: 'occupied',
|
||||
seat,
|
||||
player: playerModel(player),
|
||||
ready: player.isprepare === 1,
|
||||
offline: player.onstate === 1,
|
||||
}));
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
page: 'room',
|
||||
roomcode: room.roomcode,
|
||||
selfSeat: room.selfSeat,
|
||||
seats: Object.freeze(seats),
|
||||
canPrepare: state.app.phase === 'logged-in'
|
||||
&& room.needprepare === 1
|
||||
&& self.isprepare !== 1
|
||||
&& room.stage === 0,
|
||||
canExit: state.app.phase === 'logged-in'
|
||||
&& (room.stage === 0 || room.infinite === 1),
|
||||
});
|
||||
}
|
||||
|
||||
export function selectPageModel(
|
||||
page: PageId,
|
||||
state: PlatformState,
|
||||
ready: boolean,
|
||||
): PageModel {
|
||||
switch (page) {
|
||||
case 'loading':
|
||||
return Object.freeze({ page: 'loading' });
|
||||
case 'login':
|
||||
return Object.freeze({
|
||||
page: 'login',
|
||||
canLogin: ready,
|
||||
});
|
||||
case 'lobby':
|
||||
return Object.freeze({ page: 'lobby', self: playerModel(requireSelf(state)) });
|
||||
case 'room':
|
||||
return selectRoomModel(state);
|
||||
default:
|
||||
throw new Error(`Unsupported page: ${page}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { PlatformRuntime } from '../platform/runtime.ts';
|
||||
import type { ServerDenial } from '../platform/runtime-session.ts';
|
||||
|
||||
export type UiRuntime = Pick<PlatformRuntime,
|
||||
'state' | 'ready' | 'subscribeState' | 'login' | 'joinRoom' | 'prepare' | 'exitRoom'>;
|
||||
|
||||
export type PageId = 'loading' | 'login' | 'lobby' | 'room';
|
||||
|
||||
export interface PlayerModel {
|
||||
readonly playerid: number;
|
||||
readonly nickname: string;
|
||||
readonly avatar: string;
|
||||
readonly bean: number;
|
||||
}
|
||||
|
||||
export type SeatModel =
|
||||
| { readonly kind: 'empty'; readonly seat: number }
|
||||
| {
|
||||
readonly kind: 'occupied';
|
||||
readonly seat: number;
|
||||
readonly player: PlayerModel;
|
||||
readonly ready: boolean;
|
||||
readonly offline: boolean;
|
||||
};
|
||||
|
||||
export type PageModel =
|
||||
| { readonly page: 'loading' }
|
||||
| { readonly page: 'login'; readonly canLogin: boolean }
|
||||
| { readonly page: 'lobby'; readonly self: PlayerModel }
|
||||
| {
|
||||
readonly page: 'room';
|
||||
readonly roomcode: string;
|
||||
readonly selfSeat: number;
|
||||
readonly seats: readonly SeatModel[];
|
||||
readonly canPrepare: boolean;
|
||||
readonly canExit: boolean;
|
||||
};
|
||||
|
||||
export type OverlayModel =
|
||||
| { readonly kind: 'none' }
|
||||
| { readonly kind: 'reconnect' }
|
||||
| { readonly kind: 'denial'; readonly outcome: ServerDenial }
|
||||
| { readonly kind: 'kicked'; readonly data: unknown }
|
||||
| { readonly kind: 'fatal'; readonly error: Error };
|
||||
|
||||
export interface FrameScheduler {
|
||||
request(callback: () => void): () => void;
|
||||
}
|
||||
|
||||
export interface PlatformViewPort {
|
||||
render(model: PageModel): void;
|
||||
activate(page: PageId): void;
|
||||
showOverlay(model: OverlayModel): void;
|
||||
}
|
||||
Reference in New Issue
Block a user