Skip to content

Commit e56f53b

Browse files
Refactor stores
1 parent 9fdaa06 commit e56f53b

14 files changed

Lines changed: 607 additions & 653 deletions

File tree

islanders-client/src/lib/components/Map.svelte

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
<script lang="ts">
22
import { onMount } from 'svelte';
33
import { Application, Container, Graphics, Point, Assets, FederatedPointerEvent } from 'pixi.js';
4-
import { gameState, bindToWorld, sendAction } from '$lib/stores/game.svelte.ts';
5-
import {
6-
uiState,
7-
setIsBuilding,
8-
setIsMovingThief,
9-
setIsPlayingKnight,
10-
setIsStealingFromPlayers
11-
} from '$lib/stores/ui.svelte.ts';
12-
import type { BuildingType } from '$lib/stores/ui.svelte.ts';
4+
import { gameStore } from '$lib/stores/game.svelte.ts';
5+
import { uiStore } from '$lib/stores/ui.svelte';
6+
import type { BuildingType } from '$lib/stores/ui.svelte';
137
import {
148
type World,
159
type Player,
@@ -41,7 +35,7 @@
4135
let parentElement: HTMLElement | null = null;
4236
4337
const currentPlayer: Player | undefined = $derived(
44-
gameState.world?.players.find((player: Player) => player.name === gameState.playerName)
38+
gameStore.world?.players.find((player: Player) => player.name === gameStore.playerName)
4539
);
4640
4741
const hexSize = 200;
@@ -120,14 +114,14 @@
120114
let loadingPromise: Promise<void> | undefined;
121115
122116
$effect(() => {
123-
updateMap(gameState.world);
117+
updateMap(gameStore.world);
124118
});
125119
126120
$effect(() => {
127-
isBuilding = uiState.isBuilding;
128-
isMovingThief = uiState.isMovingThief;
129-
isPlayingKnight = uiState.isPlayingKnight;
130-
isPlayingRoadBuilding = uiState.isPlayingRoadBuilding;
121+
isBuilding = uiStore.isBuilding;
122+
isMovingThief = uiStore.isMovingThief;
123+
isPlayingKnight = uiStore.isPlayingKnight;
124+
isPlayingRoadBuilding = uiStore.isPlayingRoadBuilding;
131125
});
132126
133127
const toWorld = (screenPoint: { x: number; y: number }): { x: number; y: number } => {
@@ -169,7 +163,7 @@
169163
cursorGraphics.clear();
170164
cursorGraphics.removeChildren();
171165
try {
172-
await sendAction(action);
166+
await gameStore.sendAction(action);
173167
} catch (error) {
174168
console.warn('Failed to send action', error);
175169
}
@@ -182,8 +176,8 @@
182176
const hexToFind = grid.pointToHex(inWorld);
183177
const moveThiefAction = new MoveThiefAction(currentPlayer.name, hexToFind);
184178
dispatchActionClearCursor(moveThiefAction);
185-
setIsMovingThief(false);
186-
setIsStealingFromPlayers(true);
179+
uiStore.setMovingThief(false);
180+
uiStore.setStealingFromPlayers(true);
187181
};
188182
189183
const handleIsPlayingKnightClick = (event: FederatedPointerEvent) => {
@@ -193,12 +187,12 @@
193187
const hexToFind = grid.pointToHex(inWorld);
194188
const moveThiefAction = new MoveThiefDevCardAction(currentPlayer.name, hexToFind);
195189
dispatchActionClearCursor(moveThiefAction);
196-
setIsPlayingKnight(false);
197-
setIsStealingFromPlayers(true);
190+
uiStore.setPlayingKnight(false);
191+
uiStore.setStealingFromPlayers(true);
198192
};
199193
200194
const handleBuildClick = (event: FederatedPointerEvent) => {
201-
if (!worldContainer || !currentPlayer || !gameState.world || !grid) return;
195+
if (!worldContainer || !currentPlayer || !gameStore.world || !grid) return;
202196
203197
const inWorld = toWorld(event.global);
204198
const closestPoints = getTwoClosestPoints(grid, inWorld);
@@ -210,26 +204,26 @@
210204
211205
if (isBuilding === 'House') {
212206
const action =
213-
gameState.world.gameState === 'Started'
207+
gameStore.world.gameState === 'Started'
214208
? new BuildHouseAction(currentPlayer.name, coord)
215209
: new BuildHouseInitialAction(currentPlayer.name, coord);
216210
217211
dispatchActionClearCursor(action);
218-
setIsBuilding('None');
212+
uiStore.setBuilding('None');
219213
}
220214
if (isBuilding === 'City') {
221215
dispatchActionClearCursor(new BuildCityAction(currentPlayer.name, coord));
222-
setIsBuilding('None');
216+
uiStore.setBuilding('None');
223217
}
224218
if (isBuilding === 'Road' && closestPoints[1].index !== -1) {
225219
const coord2 = getMatrixCoordCorner(hexToFind, closestPoints[1].index);
226220
const action =
227-
gameState.world.gameState === 'Started'
221+
gameStore.world.gameState === 'Started'
228222
? new BuildRoadAction(currentPlayer.name, coord, coord2)
229223
: new BuildRoadInitialAction(currentPlayer.name, coord, coord2);
230224
231225
dispatchActionClearCursor(action);
232-
setIsBuilding('None');
226+
uiStore.setBuilding('None');
233227
}
234228
};
235229
@@ -599,7 +593,7 @@
599593
600594
onMount(() => {
601595
try {
602-
void bindToWorld();
596+
void gameStore.bindToWorld();
603597
} catch (error) {
604598
console.warn('Unable to bind to world socket', error);
605599
}

islanders-client/src/lib/components/PlayerSummary.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import type { Player } from '../../../../islanders-shared/lib/Player';
3-
import { getPlayerColorAsHex } from '$lib/stores/game.svelte';
3+
import { gameStore } from '$lib/stores/game.svelte';
44
55
interface PlayerInformation {
66
player?: Player;
@@ -15,7 +15,9 @@
1515
const playerName = $derived(player?.name ?? '');
1616
const points = $derived(player?.points ?? 0);
1717
const color = $derived(
18-
player && playerName ? (getPlayerColorAsHex(playerName) ?? defaultColor) : defaultColor
18+
player && playerName
19+
? (gameStore.getPlayerColorAsHex(playerName) ?? defaultColor)
20+
: defaultColor
1921
);
2022
</script>
2123

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { browser } from '$app/environment';
2+
import { socketManager } from './socket.svelte';
3+
import { env } from '$env/dynamic/public';
4+
import { gameStore } from './game.svelte';
5+
import { SocketActions, type ChatMessage } from '../../../../islanders-shared/lib/Shared';
6+
7+
export interface ChatEntry extends ChatMessage {
8+
id: string;
9+
ts: number;
10+
pending?: boolean;
11+
self?: boolean;
12+
}
13+
14+
const MAX_MESSAGES = 250;
15+
16+
class ChatStore {
17+
messages = $state<ChatEntry[]>([]);
18+
initialized = $state(false);
19+
error = $state<string | undefined>(undefined);
20+
21+
private listenersBound = false;
22+
private lastChatPath: string | undefined;
23+
24+
private ensureInit(gameId?: string) {
25+
if (!browser) return;
26+
this.bindChat(gameId);
27+
}
28+
29+
private bindChat(gameId?: string) {
30+
const resolved = gameId ?? gameStore.gameId;
31+
const host = env.PUBLIC_SERVER ?? 'http://localhost:3002';
32+
const path = resolved ? `${host}/${resolved}` : undefined;
33+
34+
if (this.listenersBound && path === this.lastChatPath) {
35+
return socketManager.getSocket();
36+
}
37+
38+
const socket = socketManager.connect(path);
39+
this.lastChatPath = path;
40+
41+
socket.on(SocketActions.chat, (incoming: ChatMessage & { clientId?: string }) => {
42+
const existing = this.messages;
43+
let idx = -1;
44+
if (incoming.clientId) {
45+
idx = existing.findIndex((m) => m.id === incoming.clientId);
46+
}
47+
if (idx === -1) {
48+
idx = existing.findIndex(
49+
(m) => m.pending && m.text === incoming.text && m.user === incoming.user
50+
);
51+
}
52+
if (idx !== -1) {
53+
const copy = [...existing];
54+
copy[idx] = {
55+
...copy[idx],
56+
pending: false,
57+
ts: copy[idx].ts
58+
};
59+
this.messages = copy;
60+
} else {
61+
const entry: ChatEntry = {
62+
id: crypto.randomUUID(),
63+
ts: Date.now(),
64+
text: incoming.text,
65+
user: incoming.user,
66+
self: incoming.user === gameStore.playerName,
67+
pending: false
68+
};
69+
const next = [...existing, entry];
70+
if (next.length > MAX_MESSAGES) {
71+
next.splice(0, next.length - MAX_MESSAGES);
72+
}
73+
this.messages = next;
74+
}
75+
});
76+
77+
socket.on('connect_error', (err: Error) => {
78+
this.error = err.message;
79+
});
80+
81+
this.listenersBound = true;
82+
this.initialized = true;
83+
84+
return socket;
85+
}
86+
87+
sendMessage(text: string) {
88+
const trimmed = text.trim();
89+
if (!trimmed) return;
90+
91+
const socket = socketManager.getSocket();
92+
if (!socket || !socket.connected) {
93+
this.error = 'Not connected';
94+
return;
95+
}
96+
const playerName = gameStore.playerName ?? 'Unknown';
97+
const clientId = crypto.randomUUID();
98+
99+
const optimistic: ChatEntry = {
100+
id: clientId,
101+
ts: Date.now(),
102+
text: trimmed,
103+
user: playerName,
104+
pending: true,
105+
self: true
106+
};
107+
{
108+
const existing = this.messages;
109+
const next = [...existing, optimistic];
110+
if (next.length > MAX_MESSAGES) {
111+
next.splice(0, next.length - MAX_MESSAGES);
112+
}
113+
this.messages = next;
114+
}
115+
116+
const outgoing = { text: trimmed, user: playerName, clientId } as ChatMessage;
117+
socket.emit(SocketActions.chat, outgoing);
118+
}
119+
120+
clear() {
121+
this.messages = [];
122+
this.initialized = false;
123+
this.error = undefined;
124+
this.listenersBound = false;
125+
this.lastChatPath = undefined;
126+
}
127+
128+
init(gameId?: string) {
129+
this.ensureInit(gameId);
130+
}
131+
}
132+
133+
export const chatStore = new ChatStore();

islanders-client/src/lib/stores/chat.ts

Lines changed: 0 additions & 120 deletions
This file was deleted.

0 commit comments

Comments
 (0)