8000 GitHub - jonlovera/web-sdk: About The Web Game Engine: Create your own web game with in a declarative way.
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

jonlovera/web-sdk

 
 

Repository files navigation

Web SDK

This is a web sdk that is convenient for you to develop a game in a declarative way. It is an optional way to build and launch your games on with Stake Engine with some easy steps. It is powered by Svelte 5, PixiJS 8 and TurboRepo.

  • How to use: Start your own codebase based on this repo and changing any source code as you need.

Table of Contents

Get started

Here is a complete tutorial to start with one of our sample games from storybook running and local test to build and launch it on Stake Engine. Please ignore those steps that you already know or done.

Installation

We use VSCode as IDE but this is optional and it is up to you.

  • Install node with version 22.16.0. download
# Download and install nvm:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# in lieu of restarting the shell
\. "$HOME/.nvm/nvm.sh"

# Download and install Node.js:
nvm install 22.16.0

# Verify the node versions. Should print "v22.16.0".
node -v
  • Install pnpm with version 10.5.0.
# Install pnpm
npm install pnpm@10.5.0 -g

# Verify the pnpm versions. Should print "v10.5.0"
pnpm -v
  • Clone the repo to your local in VS Code terminal or others.
git clone https://github.com/StakeEngine/web-sdk.git
cd web-sdk
  • Install dependencies.
pnpm install

WIth out any error messages showing up, you are good to continue.

Run in Storybook

pnpm run storybook --filter=lines
  • Run pnpm run storybook --filter=<MODULE_NAME> in the terminal to see the storybook of a sample game in a TurboRepo way. <MODULE_NAME> is the name in the package.json file of a module in apps or packages folders.

  • For example, we have "name": "lines" in the apps/lines/package.json, so we can find it and run its storybook by:

  • You should see this:

isolated

  • Now switch to MODE_BASE/book/random in the left sidebar, you will see an Action button appear on the left right conner of the game.

isolated

  • Click on the Action button and wait for a base game to finish.
  • Now you are having a game running locally in the storybook.

Run in DEV Mode

pnpm run dev --filter=lines
  • Open up the url showed in the terminal, you should see this:

isolated

  • It is all good to have that error screen for now, because we have not connected to the RGS with DEV mode. We will show you later how to do that.

  • For more information about how the authentication is handled, please refer to Authenticate.svelte

Build a game

pnpm run build --filter=lines
  • You should see an index.html output here apps/lines/.svelte-kit/output/prerendered/pages/index.html.

  • Create a new folder anywhere you prefer and put the index.html file there. For example:

build
  |-index.html
  • Then copy/paste everything in apps/lines/.svelte-kit/output/client to the same folder like:
build
  |-index.html
  |-_app
  |-assets
  |-favicon.svg
  |-loader.gif
  |-stake-engine-loader.gif
  • Now you are ready to upload a game!

Launch a game

  • Login Stake Engine account. Go to Files page of a game. Import files by uploading your the frontend by selecting the whole folder.

isolated

isolated

  • Then remember to click on the Publish Game button then select Front End.

isolated

  • Then go to the Developer page, click on Start game session button and click on Launch in New Tab button.

isolated

  • A game is live with a staging environment and you can play with all the buttons.

  • Check the query string in the URL and those are the values required to connect to the RGS.

  • Now you can go back to the DEV mode and copy/paste the query strings in the url and play it locally.

Congratulations! You've completed the tutorial. You can explore the more content in the following documentation for details.

Dependencies

Besides basic web skills (html, css and javascript), here it shows a list of npm dependencies of this repo. It would be great to start with understanding them before kicking off Get Started.

Explore Storybook

Storybook is a powerful and handy tool to test our games. For example:

  • COMPONENTS/<Game>/component: It tests the <Game /> component. In this case, it doesn't skip the loading screen.
  • COMPONENTS/<Game>/preSpin: It tests the <Game /> component with the preSpin function.
  • COMPONENTS/<Game>/emitterEvent: It tests the <Game /> component with an emitterEvent "boardHide".
  • ...
  • COMPONENTS/<Symbol>/component: It tests the <Symbol /> component with controls e.g. state of the symbol.
  • COMPONENTS/<Symbol>/symbols: It tests the <Symbol /> component with all the symbols and all the states.
  • ...
  • MODE_BASE/book/random: It tests the <Game /> component with a random book of base mode.
  • MODE_BASE/bookEvent/reveal: It tests the <Game /> component with a "reveal" bookEvent of the base mode. It will spin the reels.
  • ...
  • MODE_BONUS/book/random: It tests the <Game /> component with a random book of bonus mode.
  • MODE_BONUS/bookEvent/reveal: It tests the <Game /> component with a "reveal" bookEvent of the bonus mode. It will spin the reels.
  • ...

isolated

isolated

With all the stories above and the stories that created and customised by yourself, we are able to test the whole game, intermediate components and atomic components.

We are also able to test our game with a book, a sequence of bookEvents and a single bookEvent. If each bookEvent is implemented well with emitterEvents and its story is resolved properly, the game is technically finished.

Flow Chart

Here it is a simplified flow chart of steps how a game is processed after RGS request. The real situation might be more complicated, but it follows the same idea.

isolated

playBookEvents()

This function is created by packages/utils-book/src/createPlayBookUtils.ts. It goes through bookEvents one by one, handles each one with async function playBookEvent(). It resolves them one after another with sequence() in the order of the bookEvents array. It means the sequence of bookEvents matters eminently and it determines the behaviors of the game. For example, we don't want to see the "win" before "spin", so we should put "win" after the "spin". This function is also used in the MODE_<GAME_MODE>/book/random stories.

  • playBookEvent(): This is a function that takes in a bookEvent with some context (usually all the bookEvents), then find the bookEventHandler in bookEventHandlerMap based on bookEvent.type to process it. This function is also used in the MODE_<GAME_MODE>/bookEvent/<BOOK_EVENT_TYPE> stories.

  • sequence(): This is an async function to achieve resolving async functions/promises one after another. On the contrast, Promise.all() will trigger all the async functions/promises together at the same time, which is not what we desire for the sequence of the game.

bookEvent

  • book: A book is a json data that is returned from the RGS (Remote Game Server) for each game requested. It is randomly picked from over a million of books, which is math. It is mainly composed by bookEvents.
// base_books.ts - Example of a base game book

{
  id: 1,
  payoutMultiplier: 0.0,
  events: [
    {
      index: 0,
      type: 'reveal',
      board: [
        [{ name: 'L2' }, { name: 'L1' }, { name: 'L4' }, { name: 'H2' }, { name: 'L1' }],
        [{ name: 'H1' }, { name: 'L5' }, { name: 'L2' }, { name: 'H3' }, { name: 'L4' }],
        [{ name: 'L3' }, { name: 'L5' }, { name: 'L3' }, { name: 'H4' }, { name: 'L4' }],
        [{ name
ED4F
: 'H4' }, { name: 'H3' }, { name: 'L4' }, { name: 'L5' }, { name: 'L1' }],
        [{ name: 'H3' }, { name: 'L3' }, { name: 'L3' }, { name: 'H1' }, { name: 'H1' }],
      ],
      paddingPositions: [216, 205, 195, 16, 65],
      gameType: 'basegame',
      anticipation: [0, 0, 0, 0, 0],
    },
    { index: 1, type: 'setTotalWin', amount: 0 },
    { index: 2, type: 'finalWin', amount: 0 },
  ],
  criteria: '0',
  baseGameWins: 0.0,
  freeGameWins: 0.0,
}
  • bookEvent: A bookEvent is a json data that is one of the element of the book.events array.
// base_books.ts - Example of a "reveal" bookEvent

{
  index: 0,
  type: 'reveal',
  board: [
    [{ name: 'L2' }, { name: 'L1' }, { name: 'L4' }, { name: 'H2' }, { name: 'L1' }],
    [{ name: 'H1' }, { name: 'L5' }, { name: 'L2' }, { name: 'H3' }, { name: 'L4' }],
    [{ name: 'L3' }, { name: 'L5' }, { name: 'L3' }, { name: 'H4' }, { name: 'L4' }],
    [{ name: 'H4' }, { name: 'H3' }, { name: 'L4' }, { name: 'L5' }, { name: 'L1' }],
    [{ name: 'H3' }, { name: 'L3' }, { name: 'L3' }, { name: 'H1' }, { name: 'H1' }],
  ],
  paddingPositions: [216, 205, 195, 16, 65],
  gameType: 'basegame',
  anticipation: [0, 0, 0, 0, 0],
}

// base_books.ts - Example of a setTotalWin bookEvent

{ index: 1, type: 'setTotalWin', amount: 0 },
  • bookEventHandler: An async function that takes in a bookEvent and do some operations with it. Usually it broadcasts some emitterEvents, so the components will receive and handle.

bookEventHandlerMap

An object that the key is bookEvent.type and value is a bookEventHandler. We can find an example in apps/lines/src/game/bookEventHandlerMap.ts.

// bookEventHandlerMap.ts - Example of "updateFreeSpin" bookEventHandler

export const bookEventHandlerMap: BookEventHandlerMap<BookEvent, BookEventContext> = {
  ...,
  updateFreeSpin: async (bookEvent: BookEventOfType<'updateFreeSpin'>) => {
    eventEmitter.broadcast({ type: 'freeSpinCounterShow' });
    eventEmitter.broadcast({
      type: 'freeSpinCounterUpdate',
      current: bookEvent.amount,
      total: bookEvent.total,
    });
  },
  ...,
}
  • In simple terms, a book is composed by multiple bookEvents. Different combinations of bookEvents will determine the different behaviours of a game e.g. win/lose, a big/small win, a base/bonus game, 1/10/15 spins and so on.

eventEmitter

It achieves event-driven programming for the development. It can either broadcast or subscribe to emitterEvents. It connects the javascript scope and svelte component scope with emitterEvents instead of passing the different states as svelte component props directly. The three most used functions are:

  • eventEmitter.broadcast()
  • eventEmitter.broadcastAsync()
  • eventEmitter.subscribeOnMount()

emitterEvent

An emitterEvent is a json data that eventEmitter.broadcast(emitterEvent) or eventEmitter.broadcastAsync(emitterEvent) broadcasts, so that a component which has eventEmitter.subscribeOnMount(emitterEventHandlerMap) can receive the data and deal with it in a synchronous or asynchronous way.

For a game we have many animations, so sometimes we need to "await" for those animations to finish before going to the next step.

Conceptually a bookEvent is composed by emitterEvents. Nevertheless, the flexibility lies in that the emitterEvents composing a bookEvent can come from multiple different svelte components. This way we can achieve and control the interactions and timing between different svelte components for the same bookEvent, ultimately, to achieve our games.

// bookEventHandlerMap.ts - Example of an emitterEvent

{
  type: 'freeSpinCounterUpdate',
  current: undefined,
  total: bookEvent.totalFs,
}
  • EmitterEventHandler (Synchronous): A sync function that takes in an emitterEvent. It usually deals with some sync operations e.g. show/hide component, tidy up, update some numbers and so on.
// bookEventHandlerMap.ts - Example of broadcast

eventEmitter.broadcast({
  type: 'freeSpinCounterUpdate',
  current: undefined,
  total: bookEvent.totalFs,
});

// FreeSpinCounter.svelte - Example of receiving

context.eventEmitter.subscribeOnMount({
  ...,
  freeSpinCounterUpdate: (emitterEvent) => {
    if (emitterEvent.current !== undefined) current = emitterEvent.current;
    if (emitterEvent.total !== undefined) total = emitterEvent.total;
  },
  ...,
});
  • EmitterEventHandler (Asynchronous): An async function that takes in an emitterEvent. It usually deals with some async operations e.g. wait for fading in/out component, wait for animations to finish, wait for numbers to increase/decrease with svelte-tween and so on.
// bookEventHandlerMap.ts - Example of broadcastAsync

await eventEmitter.broadcastAsync({
  type: 'freeSpinIntroUpdate',
  totalFreeSpins: bookEvent.totalFs,
});

// FreeSpinIntro.svelte - Example of receiving

context.eventEmitter.subscribeOnMount({
  ...,
  freeSpinIntroUpdate: async (emitterEvent) => {
    freeSpinsFromEvent = emitterEvent.totalFreeSpins;
    await waitForResolve((resolve) => (
  },
  ...,
});

emitterEventHandlerMap

An object that the key is emitterEvent.type and value is an emitterEventHandler. We can find this object in each component. For example, apps/lines/src/components/FreeSpinCounter.svelte.

  • Each emitterEventHandler can do a lot or a little, but we prefer each emitterEventHandler just doing a minimum job to achieve the duty that is described by its type. This way we follow the Single Responsibility Principle of SOLID. For example, freeSpinCounterShow just shows this component and does nothing more.
// FreeSpinCounter.svelte and its emitterEventHandlers

<script lang="ts" module>
  export type EmitterEventFreeSpinCounter =
    | { type: 'freeSpinCounterShow' }
    | { type: 'freeSpinCounterHide' }
    | { type: 'freeSpinCounterUpdate'; current?: number; total?: number };
</script>

<script lang="ts">
  ...

  context.eventEmitter.subscribeOnMount({
    freeSpinCounterShow: () => (show = true),
    freeSpinCounterHide: () => (show = false),
    freeSpinCounterUpdate: (emitterEvent) => {
      if (emitterEvent.current !== undefined) current = emitterEvent.current;
      if (emitterEvent.total !== undefined) total = emitterEvent.total;
    },
  });
</script>

<MainContainer>
  ...
</MainContainer>

Task Breakdown

There is one single idea that is been applied across the whole carrot-game-sdk that is Task Breakdown.

To extend a bit more of the topic above, if an emitterEventHandler does too much work, then it is better we consider to split it into smaller emitterEventHandlers as a process of task-breakdown.

For example, "tumbleBoard" bookEvent is a fairly complicated bookEvent. Instead of having one "tumbleBoard" emitterEvent, we split it into "tumbleBoardInit", "tumbleBoardExplode", "tumbleBoardRemoveExploded", "tumbleBoardSlideDown".

This way we can implement a big and complicated emitterEvent step by step. More importantly, we can test the implementations one by one in storybook of COMPONENTS/<Game>/emitterEvent.

// bookEventHandlerMap.ts - Example of task-breakdown

{
  ...,
  tumbleBoard: async (bookEvent: BookEventOfType<'tumbleBoard'>) => {
    eventEmitter.broadcast({ type: 'tumbleBoardShow' });
    eventEmitter.broadcast({ type: 'tumbleBoardInit', addingBoard: bookEvent.newSymbols });
    await eventEmitter.broadcastAsync({
      type: 'tumbleBoardExplode',
      explodingPositions: bookEvent.explodingSymbols,
    });
    eventEmitter.broadcast({ type: 'tumbleBoardRemoveExploded' });
    await eventEmitter.broadcastAsync({ type: 'tumbleBoardSlideDown' });
    eventEmitter.broadcast({
      type: 'boardSettle',
      board: stateGameDerived
        .tumbleBoardCombined()
        .map((tumbleReel) => tumbleReel.map((tumbleSymbol) => tumbleSymbol.rawSymbol)),
    });
    eventEmitter.broadcast({ type: 'tumbleBoardReset' });
    eventEmitter.broadcast({ type: 'tumbleBoardHide' });
  },
  ...,
}
// TumbleBoard.svelte - Example of task-breakdown

context.eventEmitter.subscribeOnMount({
  tumbleBoardShow: () => {},
  tumbleBoardHide: () => {},
  tumbleBoardInit: () => {},
  tumbleBoardReset: () => {},
  tumbleBoardExplode: () => {},
  tumbleBoardRemoveExploded: () => {},
  tumbleBoardSlideDown: () => {},
});

Stateless games can be complicated as well (vs. stateful games). For example, a slots game can have different types of spins, number of spins, win rules, number of bookEvents, game modes, global multiplier, multiplier symbols and so on.

  • Stateless games: A single request to the RGS will finish the job of playing a game. For example, it requires only one request to play and finish a slots game.
  • Stateful games: It requires multiple requests to the RGS to be able to finish the job. For example, a mines game.

However with the data structure of math and the functions we have, we are able to break down a complicated game into small and atomic tasks (emitterEvents). It enables us to test the atomics independently as well. Visually it is something like this:

isolated

The colors of the emitterEvents under a bookEvent can be different, which means they are from different svelte components.

Steps to Add a New BookEvent

For example, we have a game apps/lines already. Assume that we have added a new bookEvent updateGlobalMult to the bonus game mode (MODE_BONUS) in math, so that we have a new global multiplier feature for the game. Based on that, here we will go through the steps together to implement this new bookEvent and add it to the game. Along the way we will introduce part of our file structure as well.

  • apps/lines/src/stories/data/bonus_books.ts: This file includes the an array of bonus books that story MODE_BONUS/book/random will randomly pick at. This is to simulate requesting data from RGS. All we need to do is to copy/paste data from our new math package and format it.
// bonus_books.ts

{
  type: 'updateGlobalMult',
  globalMult: 3,
},
  • apps/lines/src/stories/data/bonus_events.ts: This file includes the an object of every type of bookEvent that story MODE_BONUS/bookEvent/<BOOK_EVENT_TYPE> uses. All we need to do is to copy/paste data from our new math package and format it.
// bonus_events.ts

export default {
  ...,
  updateGlobalMult: {
    type: 'updateGlobalMult',
    globalMult: 3,
  },
  ...,
}
  • apps/lines/src/stories/ModeBonusBookEvent.stories.svelte: This file implements all the sub stories in story set MODE_BONUS/bookEvent. With the following code added in this file, you will see the a new story MODE_BONUS/bookEvent/updateGlobalMult that is added in our storybook with an Action button. Now if we click on it and nothing would happen, but it is a good start because we set up the testing environment first. Next step is to add code of bookEventHandler to handle it.
// ModeBonusBookEvent.stories.svelte

<Story
  name="updateGlobalMult"
  args={templateArgs({
    skipLoadingScreen: true,
    data: events.updateGlobalMult,
    action: async (data) => await playBookEvent(data, { bookEvents: [] }),
  })}
/>
// typesBookEvent.ts

type BookEventUpdateGlobalMult = {
  index: number;
  type: 'updateGlobalMult';
  globalMult: number;
};

export type BookEvent =
  | ...
  | BookEventUpdateGlobalMult
  | ...
;
  • apps/lines/src/game/bookEventHandlerMap.ts: This file includes all the bookEventHandlers. Let is add a new one for the new bookEvent. Check the intellisense that the previous step brings, it provides a better developer experience.

isolated

  • apps/lines/src/components/GlobalMultiplier.svelte: This file is created as our target svelte component for updateGlobalMulti bookEvent. Technically speaking, all the jobs that is related to global multiplier of the game should only be in this svelte component. Similar to the bookEvent types, let is add the typescript types for new emitterEvents first.
    • type EmitterEventGlobalMultiplier is a union type of EmitterEvent types.
// GlobalMultiplier.svelte

<script lang="ts" module>
  export type EmitterEventGlobalMultiplier =
    | { type: 'globalMultiplierShow' }
    | { type: 'globalMultiplierHide' }
    | { type: 'globalMultiplierUpdate'; multiplier: number };
</script>
  • apps/lines/src/game/typesEmitterEvent.ts: This file has typescript types of all the emitterEvents of the game. Let is add the type of our new emitterEvents for intellisense.
    • type EmitterEventGame is a union type of EmitterEvent types.
// typesEmitterEvent.ts

...
import type { EmitterEventGlobalMultiplier } from '../components/GlobalMultiplier.svelte';
...

export type EmitterEventGame =
  | ...
  | EmitterEventGlobalMultiplier
  | ...
;
  • apps/lines/src/game/eventEmitter.ts: This file exports the eventEmitter, it uses the EmitterEventGame and other EmitterEvent types to compose type EmitterEvent.
    • type EmitterEvent is a union type of EmitterEvent types.
// eventEmitter.ts

...
import type { EmitterEventGame } from './typesEmitterEvent';
export type EmitterEvent = EmitterEventUi | EmitterEventHotKey | EmitterEventGame;
export const { eventEmitter } = createEventEmitter<EmitterEvent>();

isolated

// GlobalMultiplier.svelte

<script lang="ts" module>
  export type EmitterEventGlobalMultiplier =
    | { type: 'globalMultiplierShow' }
    | { type: 'globalMultiplierHide' }
    | { type: 'globalMultiplierUpdate'; multiplier: number };
</script>

<script lang="ts">
  ...

  context.eventEmitter.subscribeOnMount({
    globalMultiplierShow: () => (show = true),
    globalMultiplierHide: () => (show = false),
    globalMultiplierUpdate: async (emitterEvent) => {
      console.log(emitterEvent.multiplier)
    },
  });
</script>

<SpineProvider key="globalMultiplier" width={PANEL_WIDTH}>
  ...
  <SpineTrack trackIndex={0} {animationName} />
</SpineProvider>
  • Test it individually (MODE_BONUS/bookEvent/updateGlobalMult): Run storybook and we should see this a new story "updateGlobalMult" has been added.

    • Now click on the Action button and we should see the <GlobalMultiplier /> component animates correctly followed by the " ⓘ Action is resolved ✅ " message, otherwise we need to go back to the component and figure out what is wrong until it is resolved.

    • If you find out the component hard to debug, we'd better start creating a new story COMPONENTS/<GlobalMultiplierSpine>/component. <GlobalMultiplierSpine /> component will purely take props and achieve its duty instead of being controlled by emitterEvents. This way it becomes more friendly for testing the component with the storybook controls.

  • Test it in books (MODE_BONUS/book/random): Final step is to test it in a book environment by switching to this book story. In a previous step we have updated apps/lines/src/stories/data/bonus_books.ts, so the new bookEvent will appear if we keep hitting the Action button in this story.

File Structure

The file structure is in a way of structure of TurboRepo to achieve a monorepo. Besides the files for the configurations of TurboRepo, sveltekit, eslint, typescript, git and so on, here is a list of of key modules of apps and packages.

root
  |_apps
  |  |_cluster
  |  |_lines
  |  |_price
  |  |_scatter
  |  |_ways
  |
  |_packages
     |_config-*
     |_constants-*
     |_state-*
     |_utils-*
     |_components-*
     |_pixi-*

For each game, it has an individual folder in the apps, for example apps/lines.

{
  "name": "lines",
  ...
}
  • To run the app in DEV mode instead of in the storybook: Run pnpm run dev --filter=<MODULE_NAME> in the terminal.
pnpm run dev --filter=lines
  • apps/lines/src/routes/+page.svelte: This is the entry file of sample game apps/lines in a sveltekit way. It is a combination of two things:
    • setContext(): A function that sets all the svelte-context required and used in this app and in the packages. As we already know, only children-level components can access the context. That is why we set the context at the entry level of the app.
    • <Game />: The entry svelte component to the game. It includes all the components of the game.
// +page.svelte

<script lang="ts">
  import Game from '../components/Game.svelte';
  import { setContext } from '../game/context';

  setContext();
</script>

<Game />
// ComponentsGame.stories.svelte

<script lang="ts">
  ...
  import Game from '../components/Game.svelte';
  import { setContext } from '../game/context';

  ...
  setContext();
</script>

<Story name="component (loadingScreen)">
  <StoryLocale lang="en">
    <Game />
  </StoryLocale>
</Story>
  • We can render <Game /> component in the app or in the storybook. Either way it requires the context to set in advance, otherwise the children or the descendants will throw errors if they use the "getContext()" from apps or "getContext()" from packages.

For every TurboRepo local package, you can import and use them in an app or in another local package directly without publishing them to npm. Our codebase benefits considerably from a monorepo because it brings reusability, readability, maintainability, code splitting and so on. Here is an example of importing local packages with workspace:* in apps/lines/package.json:

// package.json

{
  "name": "lines",
  ...,
  "devDependencies": {
    ...,
    "config-ts": "workspace:*",
  },
  "dependencies": {
    ...,
    "pixi-svelte": "workspace:*",
    "constants-shared": "workspace:*",
    "state-shared": "workspace:*",
    "utils-shared": "workspace:*",
    "components-shared": "workspace:*",
  }
}

The naming convention of packages is a combination of <PACKAGE_TYPE>, hyphen and <SPECIAL_DEPENDENCY> or <SPECIAL_USAGE>. For example, components-pixi is a local package that the package type is "components" and the special dependency is pixi-svelte.

  • config-*:
  • pixi-*
  • constants-*:
  • state-*:
  • utils-*:
    • utils-book: This local package contains reusable functions/types that are related to book and bookEvent.
    • utils-fetcher: This local package contains reusable functions/types based on fetch API.
    • utils-shared: This local package contains reusable functions/types, except for lodash and lingui.
    • utils-slots: This local package contains reusable functions/types for slots game, for example creating reel and spinning the board.
    • utils-sound: This local package contains reusable functions/types based on npm package howler for music and sound effect.
    • utils-event-emitter: This local package contains reusable functions/types to achieve our event-driven programming.
    • utils-xstate: This local package contains reusable functions/types based on npm package xstate.
      • It creates stateXstate, stateXstateDerived and ContextXstate as a svelte-context
    • utils-layout: This local package contains reusable functions/types for our layout system of pixijs.
      • It creates stateLayout, stateLayoutDerived and ContextLayout as a svelte-context
  • components-*:
    • components-layout: This local package contains reusable svelte components based on another local package utils-layout.
    • components-pixi: This local package contains reusable svelte components based on pixi-svelte.
    • components-shared: This local package contains reusable svelte components based on html.
    • components-storybook: This local package contains reusable svelte components for storybooks.
    • components-ui-pixi: This local package contains reusable svelte pixi-svelte components for the game UI.
    • components-ui-html: This local package contains reusable svelte html components for the game UI.

For *-shared packages, they are created to be reused as much as possible by other apps and packages. Instead of having a special dependency or usage, they should have a minimum list of dependencies and a broad set of use cases.

pixi-svelte, utils-event-emitter, utils-layout and utils-xstate they have functions to create corresponding svelte-context. For the contexts, they can be used by either an app or a local components-* package by just calling the getContext<CONTEXT_NAME>(). For example, components in components-layout use getContextLayout() from utils-layout. In this way, we can regard pixi-svelte as an integration of "utils-pixi-svelte" and "components-pixi-svelte".

Context

svelte-context is a useful feature from svelte especially when a shared state requires some inputs/types to create. Here it shows the structure of context of sample game apps/lines. As showed before, setContext() is called at entry level component. For example, apps/lines/src/routes/+page.svelte or apps/lines/src/stories/ComponentsGame.stories.svelte. It sets four major contexts from the packages by this:

// context.ts - Example of setContext in apps

export const setContext = () => {
  setContextEventEmitter<EmitterEvent>({ eventEmitter });
  setContextXstate({ stateXstate, stateXstateDerived });
  setContextLayout({ stateLayout, stateLayoutDerived });
  setContextApp({ stateApp });
};

Different apps and packages require different contexts.

isolated

ContextEventEmitter

eventEmitter is created by packages/utils-event-emitter/src/createEventEmitter.ts. We have covered eventEmitter in the previous content.

ContextLayout

stateLayout and stateLayoutDerived are created by packages/utils-layout/src/createLayout.svelte.ts. It provides canvasSizes, canvasRatio, layoutType and so on. Because we have a setting resizeTo: window for PIXI.Application, we use the sizes of window from svelte-reactivity as canvasSizes.

For html, the tags will auto-flow by default. However, in the canvas/pixijs we need to set positions manually to avoid overlapping. The importance of LayoutContext is that it provides us the values of boundaries (canvasSizes), device type based on the dimensions (layoutType) and so on. For example:

  • Set a pixi-svelte component to the left edge of the canvas:
    • <Component x={0} />
  • Set a pixi-svelte component to the right edge of the canvas:
    • <Component x={context.stateLayoutDerived.canvasSizes().width} anchor={{ x: 1: y: 0 }} />
    • It works when <App /> is the parent of the component, otherwise it will be determined by its parent <Container />.
    • The reason why we set anchor is because that the drawing is always go from top-left to bottom-right in pixijs.
// createLayout.svelte.ts

import { innerWidth, innerHeight } from 'svelte/reactivity/window';

...

const stateLayout = $state({
  showLoadingScreen: true,
});

const stateLayoutDerived = {
  canvasSizes,
  canvasRatio,
  canvasRatioType,
  canvasSizeType,
  layoutType,
  isStacked,
  mainLayout,
  normalBackgroundLayout,
  portraitBackgroundLayout,
};

ContextXstate

stateXstate and stateXstateDerived are created by packages/utils-xstate/src/createXstateUtils.svelte.ts. It provides a few functions to check the state of finite state machine, also known as gameActor, which is created by packages/utils-xstate/src/createGameActor.svelte.ts.

// createXstateUtils.svelte.ts

import { matchesState, type StateValue } from 'xstate';

...

const stateXstate = $state({
  value: '' as StateValue,
});

const matchesXstate = (state: string) => matchesState(state, stateXstate.value);

const stateXstateDerived = {
  matchesXstate,
  isRendering: () => matchesXstate(STATE_RENDERING),
  isIdle: () => matchesXstate(STATE_IDLE),
  isBetting: () => matchesXstate(STATE_BET),
  isAutoBetting: () => matchesXstate(STATE_AUTOBET),
  isResumingBet: () => matchesXstate(STATE_RESUME_BET),
  isForcingResult: () => matchesXstate(STATE_FORCE_RESULT),
  isPlaying: () => !matchesXstate(STATE_RENDERING) && !matchesXstate(STATE_IDLE),
};

gameActor: To avoid using massive "if-else" conditions in the code, we use npm/xstate to create a finite state machine to handle the complicated logic and states of betting. It provides a few pre-defined mechanics like one-off bet, autoBet with a count down, resumeBet to continue an unfinished bet and so on.

// createGameActor.svelte.ts

import { setup, createActor } from 'xstate';

...

const gameMachine = setup({
  actors: {
    bet: intermediateMachines.bet,
    autoBet: intermediateMachines.autoBet,
    resumeBet: intermediateMachines.resumeBet,
    forceResult: intermediateMachines.forceResult,
  },
}).createMachine({
  initial: 'rendering',
  states: {
    [STATE_RENDERING]: stateRendering,
    [STATE_IDLE]: stateIdle,
    [STATE_BET]: stateBet,
    [STATE_AUTOBET]: stateAutoBet,
    [STATE_RESUME_BET]: stateResumeBet,
    [STATE_FORCE_RESULT]: stateForceResult,
  },
});

const gameActor = createActor(gameMachine);

This is highly useful when it comes to the interactions with UI, for example disable the bet button when the a game is playing.

// BetButton.svelte - Example of interaction between xstate and UI

<script lang="ts">
  import { getContext } from '../context';

  const context = getContext();
</script>

<SimpleUiButton disabled={context.stateXstateDerived.isPlaying()} />

ContextApp

stateApp is created by packages/pixi-svelte/src/lib/createApp.svelte.ts. loadedAssets contains the static images, animations and sound data that is processed by PIXI.Assets.load with stateApp.assets. loadedAssets can be digested by pixi-svelte components directly as showed in pixi-svelte component <Sprite />.

// createApp.svelte.ts

const stateApp = $state({
  reset,
  assets,
  loaded: false,
  loadingProgress: 0,
  loadedAssets: {} as LoadedAssets,
  pixiApplication: undefined as PIXI.Application | undefined,
});

UI

We have provided s 5836 olutions for the UI, which are components-ui-pixi and components-ui-html. They are functional with a few features like auto gaming, turbo mode, bonus button, responsiveness and so on, although they are not as beautiful.

<script lang="ts">
	import { UI, UiGameName } from 'components-ui-pixi';
	import { GameVersion, Modals } from 'components-ui-html';
</script>

<App>
  <UI>
    {#snippet gameName()}
      <UiGameName name="LINES GAME" />
    {/snippet}
    {#snippet logo()}
      <Text
        anchor={{ x: 1, y: 0 }}
        text="ADD YOUR LOGO"
        style={{
          fontFamily: 'proxima-nova',
          fontSize: REM * 1.5,
          fontWeight: '600',
          lineHeight: REM * 2,
          fill: 0xffffff,
        }}
      />
    {/snippet}
  </UI>
</App>

<Modals>
	{#snippet version()}
		<GameVersion version="0.0.0" />
	{/snippet}
</Modals>

For the branding purpose, we recommend you to regard them as just an example of UI packages instead of applying them directly to your final product. It would be a good choice to use them as a starting point and add more style to them to build your UI. It is completely fine to ignore them and build your own UI from scratch.

Internationalisation (i18n)

About

About The Web Game Engine: Create your own web game with in a declarative way.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 98.3%
  • Svelte 1.7%
0