Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 13 additions & 9 deletions client/web/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Phaser Svelte Template

This is a Phaser 3 project template that uses the Svelte framework, TypeScript and Vite for bundling. It includes a bridge for Svelte to Phaser game communication, hot-reloading for quick development workflow and scripts to generate production-ready builds.
This is a Phaser 3 project template that uses the Svelte framework and Vite for bundling. It includes a bridge for Svelte to Phaser game communication, hot-reloading for quick development workflow and scripts to generate production-ready builds.

### Versions

Expand All @@ -9,7 +9,8 @@ This template has been updated for:
- [Phaser 3.90.0](https://github.com/phaserjs/phaser)
- [Svelte 5.23.1](https://github.com/sveltejs/kit)
- [Vite 6.3.1](https://github.com/vitejs/vite)
- [TypeScript 5.7.2](https://github.com/microsoft/TypeScript)

Source is plain JavaScript with JSDoc type annotations, checked via `npm run check` (`svelte-check` against `jsconfig.json`).

![screenshot](screenshot.png)

Expand All @@ -26,6 +27,7 @@ This template has been updated for:
| `npm run build` | Create a production build in the `build` folder |
| `npm run dev-nolog` | Launch a development web server without sending anonymous data (see "About log.js" below) |
| `npm run build-nolog` | Create a production build in the `dist` folder without sending anonymous data (see "About log.js" below) |
| `npm run check` | Type-check the JSDoc-annotated JavaScript and Svelte source via `svelte-check` |

## Writing Code

Expand All @@ -48,16 +50,16 @@ We have provided a default project structure to get you started. This is as foll
| `src/+page.svelte` | Svelte page that integrates the functionality of the game created with Phaser.|
| `src/PhaserGame.svelte` | The Svelte component that initializes the Phaser game and acts as a bridge between Svelte and Phaser.|
| `src/game` | Contains the game source code. |
| `src/game/EventBus.ts` | A simple event bus to communicate between Svelte and Phaser. |
| `src/game/main.ts` | The main **game** entry point containing the game configuration and starting the game.|
| `src/game/EventBus.js` | A simple event bus to communicate between Svelte and Phaser. |
| `src/game/main.js` | The main **game** entry point containing the game configuration and starting the game.|
| `src/game/scenes/` | Folder containing the Phaser Scenes. |
| `static/assets` | Contains the static assets used by the game. |

## Svelte Bridge

The `PhaserGame.svelte` component is the bridge between Svelte and Phaser. It initializes the Phaser game and passes events between the two.

To communicate between Svelte and Phaser, you can use the **EventBus.ts** file. This is a simple event bus that allows you to emit and listen for events from both Svelte and Phaser.
To communicate between Svelte and Phaser, you can use the **EventBus.js** file. This is a simple event bus that allows you to emit and listen for events from both Svelte and Phaser.

```js
// In Svelte
Expand Down Expand Up @@ -112,11 +114,13 @@ Here's an example of how to access Phaser data for use in a Svelte Component:

```js
// In a parent component
<script lang="ts">
import type { Scene } from "phaser";
import PhaserGame, { type TPhaserRef } from "game/PhaserGame.svelte"; // We provide the type TPhaserRef but this route is an example. You should use the correct path to the PhaserGame component.
<script>
import PhaserGame from "game/PhaserGame.svelte"; // This route is an example. You should use the correct path to the PhaserGame component.

/** @typedef {import('game/PhaserGame.svelte').TPhaserRef} TPhaserRef */

let phaserRef: TPhaserRef = { game: null, scene: null};
/** @type {TPhaserRef} */
let phaserRef = { game: null, scene: null};

const onCurrentActiveScene = (scene) => {

Expand Down
22 changes: 11 additions & 11 deletions client/web/tsconfig.node.json → client/web/jsconfig.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": [
"ES2023"
],
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"resolveJsonModule": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"checkJs": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": [
"vite.config.ts"
"src/**/*.js",
"src/**/*.d.ts",
"src/**/*.svelte"
]
}
}
1 change: 1 addition & 0 deletions client/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"dev-nolog": "vite --config vite/config.dev.mjs",
"build-nolog": "vite build --config vite/config.prod.mjs",
"preview": "vite preview --outDir build",
"check": "svelte-check --tsconfig ./jsconfig.json",
"test": "vitest run",
"test:watch": "vitest"
},
Expand Down
27 changes: 14 additions & 13 deletions client/web/src/PhaserGame.svelte
Original file line number Diff line number Diff line change
@@ -1,40 +1,41 @@
<script context="module" lang="ts">
<script context="module">

import type { Game, Scene } from "phaser";

export type TPhaserRef = {
game: Game | null,
scene: Scene | null
};
/**
* @typedef {Object} TPhaserRef
* @property {import('phaser').Game | null} game
* @property {import('phaser').Scene | null} scene
*/

</script>

<script lang="ts">
<script>

import { onMount } from "svelte";
import StartGame from "./game/main";
import { EventBus } from './game/EventBus';

export let phaserRef: TPhaserRef = {
/** @type {TPhaserRef} */
export let phaserRef = {
game: null,
scene: null
};

export let currentActiveScene: (scene: Scene) => void | undefined;
/** @type {(scene: import('phaser').Scene) => void | undefined} */
export let currentActiveScene;

onMount(() => {

phaserRef.game = StartGame("game-container");

EventBus.on('current-scene-ready', (scene_instance: Scene) => {
EventBus.on('current-scene-ready', (/** @type {import('phaser').Scene} */ scene_instance) => {

phaserRef.scene = scene_instance;

if(currentActiveScene)
{

currentActiveScene(scene_instance);

}

});
Expand Down
2 changes: 2 additions & 0 deletions client/web/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/// <reference types="vite/client" />

// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
// onAuthStateChanged listener, so reactive consumers get repaint when the
// SDK rehydrates the user from persistence.

import { initFirebase, onAuthStateChanged, signInWithGoogle, signInWithEmail, signInAnonymously, signOut, getIdToken, type User } from './firebase';
import { initFirebase, onAuthStateChanged, signInWithGoogle, signInWithEmail, signInAnonymously, signOut, getIdToken } from './firebase';

/** @typedef {import('./firebase').User} User */

class AuthState {
user = $state<User | null>(null);
user = $state(/** @type {User | null} */ (null));
loading = $state(true);
error = $state<string | null>(null);
error = $state(/** @type {string | null} */ (null));

constructor() {
const a = initFirebase();
Expand All @@ -23,16 +25,20 @@ class AuthState {
try {
this.user = await signInWithGoogle();
} catch (e) {
this.error = (e as Error).message;
this.error = /** @type {Error} */ (e).message;
}
}

async signInWithEmail(email: string, password: string) {
/**
* @param {string} email
* @param {string} password
*/
async signInWithEmail(email, password) {
this.error = null;
try {
this.user = await signInWithEmail(email, password);
} catch (e) {
this.error = (e as Error).message;
this.error = /** @type {Error} */ (e).message;
}
}

Expand All @@ -41,7 +47,7 @@ class AuthState {
try {
this.user = await signInAnonymously();
} catch (e) {
this.error = (e as Error).message;
this.error = /** @type {Error} */ (e).message;
}
}

Expand All @@ -50,7 +56,11 @@ class AuthState {
this.user = null;
}

async getIdToken(forceRefresh = false): Promise<string | null> {
/**
* @param {boolean} [forceRefresh]
* @returns {Promise<string | null>}
*/
async getIdToken(forceRefresh = false) {
return getIdToken(forceRefresh);
}
}
Expand Down
42 changes: 29 additions & 13 deletions client/web/src/auth/firebase.ts → client/web/src/auth/firebase.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,28 @@
// Public-by-design config (apiKey etc.) lives in `import.meta.env.VITE_*`
// — safe to ship in the bundle.

import { initializeApp, type FirebaseApp } from 'firebase/app';
import { initializeApp } from 'firebase/app';
import {
getAuth,
signInWithPopup,
signInWithEmailAndPassword,
signInAnonymously as fbSignInAnon,
GoogleAuthProvider,
signOut as fbSignOut,
onAuthStateChanged,
type User,
type Auth
onAuthStateChanged
} from 'firebase/auth';

let app: FirebaseApp | null = null;
let auth: Auth | null = null;
/** @typedef {import('firebase/app').FirebaseApp} FirebaseApp */
/** @typedef {import('firebase/auth').User} User */
/** @typedef {import('firebase/auth').Auth} Auth */

export function initFirebase(): Auth {
/** @type {FirebaseApp | null} */
let app = null;
/** @type {Auth | null} */
let auth = null;

/** @returns {Auth} */
export function initFirebase() {
if (auth) return auth;
app = initializeApp({
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
Expand All @@ -33,37 +38,48 @@ export function initFirebase(): Auth {
return auth;
}

export async function signInWithGoogle(): Promise<User> {
/** @returns {Promise<User>} */
export async function signInWithGoogle() {
const a = initFirebase();
const cred = await signInWithPopup(a, new GoogleAuthProvider());
return cred.user;
}

export async function signInWithEmail(email: string, password: string): Promise<User> {
/**
* @param {string} email
* @param {string} password
* @returns {Promise<User>}
*/
export async function signInWithEmail(email, password) {
const a = initFirebase();
const cred = await signInWithEmailAndPassword(a, email, password);
return cred.user;
}

export async function signInAnonymously(): Promise<User> {
/** @returns {Promise<User>} */
export async function signInAnonymously() {
const a = initFirebase();
const cred = await fbSignInAnon(a);
return cred.user;
}

export async function signOut(): Promise<void> {
/** @returns {Promise<void>} */
export async function signOut() {
const a = initFirebase();
await fbSignOut(a);
}

// getIdToken returns the current ID token. Pass forceRefresh=true to bypass
// the SDK's 5-minute cache (the WS reconnect path uses this).
export async function getIdToken(forceRefresh = false): Promise<string | null> {
/**
* @param {boolean} [forceRefresh]
* @returns {Promise<string | null>}
*/
export async function getIdToken(forceRefresh = false) {
const a = initFirebase();
const u = a.currentUser;
if (!u) return null;
return u.getIdToken(forceRefresh);
}

export { onAuthStateChanged };
export type { User };
5 changes: 3 additions & 2 deletions client/web/src/components/BetaBanner.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script lang="ts">
<script>
// Sticky beta-stage notice. `mode` ∈ "full" (signin page, always visible)
// or "topbar" (lobby, dismissable per session via sessionStorage).
let { mode = 'full' as 'full' | 'topbar' } = $props();
/** @type {{ mode?: 'full' | 'topbar' }} */
let { mode = 'full' } = $props();

const sessionKey = 'dleague.beta.dismissed';
let dismissed = $state(
Expand Down
2 changes: 1 addition & 1 deletion client/web/src/components/Lobby.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script lang="ts">
<script>
import { onMount, onDestroy } from 'svelte';
import { auth } from '../auth/auth.svelte';
import { WsClient, defaultWsUrl } from '../net/ws';
Expand Down
4 changes: 2 additions & 2 deletions client/web/src/components/SignIn.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script lang="ts">
<script>
import { auth } from '../auth/auth.svelte';
import BetaBanner from './BetaBanner.svelte';

Expand Down Expand Up @@ -36,7 +36,7 @@

<form
class="email"
onsubmit={(e: SubmitEvent) => {
onsubmit={(/** @type {SubmitEvent} */ e) => {
e.preventDefault();
withEmail();
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Events } from 'phaser';

// Used to emit events between components, HTML and Phaser scenes
export const EventBus = new Events.EventEmitter();
export const EventBus = new Events.EventEmitter();
6 changes: 4 additions & 2 deletions client/web/src/game/main.ts → client/web/src/game/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { Preloader } from './scenes/Preloader';

// Find out more information about the Game Config at:
// https://docs.phaser.io/api-documentation/typedef/types-core#gameconfig
const config: Phaser.Types.Core.GameConfig = {
/** @type {Phaser.Types.Core.GameConfig} */
const config = {
type: AUTO,
width: 1024,
height: 768,
Expand All @@ -22,7 +23,8 @@ const config: Phaser.Types.Core.GameConfig = {
]
};

const StartGame = (parent: string) => {
/** @param {string} parent */
const StartGame = (parent) => {

return new Game({ ...config, parent });

Expand Down
File renamed without changes.
Loading
Loading