Compare commits
48 Commits
ca7c3d44eb
...
2eda5a388e
Author | SHA1 | Date |
---|---|---|
Yanick Champoux | 2eda5a388e | |
Yanick Champoux | 365610184a | |
Yanick Champoux | 48292a0ca5 | |
Yanick Champoux | c1e3708b87 | |
Yanick Champoux | 4657b83fd6 | |
Yanick Champoux | d6f07a26e5 | |
Yanick Champoux | 8779d8ae31 | |
Yanick Champoux | 1919ef3850 | |
Yanick Champoux | e2dbe8364f | |
Yanick Champoux | bd5ba3ff7a | |
Yanick Champoux | 3e9ceddb48 | |
Yanick Champoux | bf23443f3a | |
Yanick Champoux | 931cb9d237 | |
Yanick Champoux | 621252329b | |
Yanick Champoux | 85ad1bcaad | |
Yanick Champoux | 8d1ec92cfd | |
Yanick Champoux | 29c1d2dc26 | |
Yanick Champoux | 05f522095f | |
Yanick Champoux | 3284e94ad4 | |
Yanick Champoux | 0b8e293613 | |
Yanick Champoux | e223d64864 | |
Yanick Champoux | c3980723f4 | |
Yanick Champoux | 0544b91872 | |
Yanick Champoux | 6e2bb0ddab | |
Yanick Champoux | 01c8430995 | |
Yanick Champoux | 929b833a49 | |
Yanick Champoux | b4f12a2347 | |
Yanick Champoux | 3cb78f03ba | |
Yanick Champoux | 2f27f76d87 | |
Yanick Champoux | c990154bad | |
Yanick Champoux | efd596410b | |
Yanick Champoux | 6bbba634c8 | |
Yanick Champoux | 59d4856904 | |
Yanick Champoux | ed3535943d | |
Yanick Champoux | 834b317b2f | |
Yanick Champoux | 7b7b1c5ad4 | |
Yanick Champoux | 80261bc524 | |
Yanick Champoux | 2a32828964 | |
Yanick Champoux | aac908afce | |
Yanick Champoux | e96a022c8f | |
Yanick Champoux | 41c6cae377 | |
Yanick Champoux | ef0da12a04 | |
Yanick Champoux | db8bb6ccc0 | |
Yanick Champoux | 286b5b9306 | |
Yanick Champoux | 5fba44a85c | |
Yanick Champoux | 98a6ce6361 | |
Yanick Champoux | 552df41139 | |
Yanick Champoux | 19f2bb9177 |
|
@ -0,0 +1,15 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
Taskfile.yaml
|
||||
.eslintignore
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: ['eslint:recommended', 'prettier'],
|
||||
plugins: ['svelte3'],
|
||||
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
pnpm-lock.yaml
|
||||
.npmrc
|
|
@ -0,0 +1,14 @@
|
|||
#exclude: static/fontawesome|^build
|
||||
|
||||
default_stages:
|
||||
- merge-commit
|
||||
- push
|
||||
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: lint
|
||||
name: lint
|
||||
entry: task lint:fix --
|
||||
language: system
|
||||
files: ''
|
|
@ -0,0 +1,13 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
|
@ -0,0 +1,12 @@
|
|||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: 'all',
|
||||
singleQuote: true,
|
||||
printWidth: 80,
|
||||
tabWidth: 4,
|
||||
useTabs: true,
|
||||
plugins: ['prettier-plugin-svelte'],
|
||||
svelteSortOrder: 'options-markup-scripts-styles',
|
||||
svelteStrictMode: false,
|
||||
svelteAllowShorthand: true,
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
|
@ -0,0 +1,30 @@
|
|||
# https://taskfile.dev
|
||||
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
GREETING: Hello, World!
|
||||
FILES:
|
||||
sh: git diff-ls --diff-filter=d main | grep -v .eslintignore
|
||||
|
||||
tasks:
|
||||
build: vite build
|
||||
dev: vite
|
||||
test: vitest run src
|
||||
test:watch: vitest src
|
||||
story: histoire dev
|
||||
default:
|
||||
cmds:
|
||||
- echo "{{.GREETING}}"
|
||||
silent: true
|
||||
lint:fix:
|
||||
cmds:
|
||||
- npx prettier --write {{.CLI_ARGS | default .FILES | catLines }}
|
||||
- npx eslint --fix --quiet {{.CLI_ARGS | default .FILES | catLines }}
|
||||
integrate:
|
||||
cmds:
|
||||
- git is-clean
|
||||
- task: lint:fix
|
||||
- task: test
|
||||
- git co main
|
||||
- git weld -
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from 'histoire';
|
||||
import { HstSvelte } from '@histoire/plugin-svelte';
|
||||
|
||||
export default defineConfig({
|
||||
setupFile: '/src/histoire.setup.js',
|
||||
plugins: [HstSvelte()],
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Under Falling Skies</title>
|
||||
<script>
|
||||
var global = global === undefined ? window : global;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": false
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "under-falling-skies",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"test": "playwright test",
|
||||
"test:unit": "vitest",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@histoire/plugin-svelte": "^0.12.4",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@sveltejs/adapter-auto": "^1.0.0",
|
||||
"@sveltejs/kit": "^1.0.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte3": "^4.0.0",
|
||||
"histoire": "^0.12.4",
|
||||
"prettier": "^2.8.2",
|
||||
"prettier-plugin-svelte": "^2.8.1",
|
||||
"svelte": "^3.54.0",
|
||||
"svelte-check": "^2.9.2",
|
||||
"vite": "^4.0.0",
|
||||
"vitest": "^0.25.8"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@ernane/svelte-star-rating": "^1.1.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.2",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"@yanick/updeep-remeda": "^2.1.0",
|
||||
"beercss": "^3.0.4",
|
||||
"dexie": "^3.2.2",
|
||||
"events": "^3.3.0",
|
||||
"fake-indexeddb": "^4.0.1",
|
||||
"material-dynamic-colors": "^0.1.5",
|
||||
"pouchdb": "^8.0.0",
|
||||
"pouchdb-adapter-memory": "^8.0.0",
|
||||
"pouchdb-browser": "^8.0.0",
|
||||
"remeda": "^1.3.0",
|
||||
"svelte-spa-router": "^3.3.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'tests'
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -0,0 +1,9 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
//global?.__SVELTEKIT_APP_VERSION_POLL_INTERVAL__ = 1000;
|
|
@ -0,0 +1,7 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('sum test', () => {
|
||||
it('adds 1 + 2 to equal 3', () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
<Beer />
|
||||
<main class="center">
|
||||
<Router {routes} />
|
||||
</main>
|
||||
|
||||
<script>
|
||||
import { genApi } from '$lib/store/api.js';
|
||||
import { writable } from 'svelte/store';
|
||||
import { setContext } from 'svelte';
|
||||
import Beer from '$lib/components/Beer.svelte';
|
||||
import Router from 'svelte-spa-router';
|
||||
import Campaigns from '$lib/components/Campaigns.svelte';
|
||||
import Campaign from '$lib/components/Campaign.svelte';
|
||||
import Battle from '$lib/components/Battle.svelte';
|
||||
import { push } from 'svelte-spa-router';
|
||||
|
||||
const api = genApi();
|
||||
|
||||
setContext('api', api);
|
||||
|
||||
const transition = writable('');
|
||||
transition.goRight = (uri) => {
|
||||
transition.set('right');
|
||||
push(uri);
|
||||
};
|
||||
transition.goLeft = (uri) => {
|
||||
transition.set('left');
|
||||
push(uri);
|
||||
};
|
||||
|
||||
setContext('transition', transition);
|
||||
|
||||
const routes = {
|
||||
// Exact path
|
||||
'/': Campaigns,
|
||||
'/campaign/:campaignId': Campaign,
|
||||
'/campaign/:campaignId/battle/:battleId': Battle,
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
main {
|
||||
max-width: 720px;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,25 @@
|
|||
<Beer />
|
||||
<Hst.Story>
|
||||
<Battle {status} params={{ battleId: 1 }} />
|
||||
|
||||
<svelte:fragment slot="controls">
|
||||
<Hst.Select
|
||||
bind:value={status}
|
||||
title="status"
|
||||
options={Object.fromEntries(statuses.map((s) => [s, s]))}
|
||||
/>
|
||||
<Hst.Number bind:value={wave} title="wave" />
|
||||
</svelte:fragment>
|
||||
</Hst.Story>
|
||||
|
||||
<script>
|
||||
/** @type any */
|
||||
export let Hst;
|
||||
import Beer from './Beer.svelte';
|
||||
import Battle from './Battle.svelte';
|
||||
|
||||
let status = 'upcoming';
|
||||
let wave = '1';
|
||||
|
||||
let statuses = ['upcoming', 'prep', 'ongoing', 'won', 'lost'];
|
||||
</script>
|
|
@ -0,0 +1,325 @@
|
|||
{#if !$activeCampaign}
|
||||
<div class="fill medium-height middle-align center-align">
|
||||
<div class="center-align">
|
||||
<h5>Loading...</h5>
|
||||
<div class="space">
|
||||
<a class="loader large" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<article class="scroll">
|
||||
<header class="fixed fill">
|
||||
<nav>
|
||||
<a href="#/">
|
||||
<button class="circle transparent">
|
||||
<i>menu</i>
|
||||
</button></a
|
||||
>
|
||||
<h5 class="max right-align">
|
||||
{#if $activeCampaign}
|
||||
<a href={`#/campaign/${$activeCampaign._id}`}>
|
||||
{$activeCampaign.name}
|
||||
</a>
|
||||
{/if}
|
||||
</h5>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{#key params.battleId}
|
||||
<div in:fly={flyParams} class="body">
|
||||
<h6>
|
||||
<span
|
||||
>Chapter {chapter},
|
||||
<span class="battleNbr">battle {chapterBattle}</span
|
||||
></span
|
||||
>
|
||||
<span class="status">{status}</span>
|
||||
</h6>
|
||||
<dl>
|
||||
{#if status === 'ongoing' && chapter !== 4}
|
||||
<dt>wave</dt>
|
||||
<dd>{wave === 2 ? 'second' : 'first'}</dd>
|
||||
{/if}
|
||||
<dt><i>home</i></dt>
|
||||
<dd>
|
||||
{#if typeof city === 'string'}
|
||||
{city}
|
||||
{:else}
|
||||
<AdditionalCharacter
|
||||
on:change={changeCity}
|
||||
selection={city?.selection}
|
||||
choices={status !== 'ongoing'
|
||||
? []
|
||||
: city?.choices?.filter(
|
||||
(c) => c !== city.selection,
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
</dd>
|
||||
<dt><i>view_kanban</i></dt>
|
||||
<dd>{scenario}</dd>
|
||||
|
||||
{#if character}
|
||||
<dt><i>person</i></dt>
|
||||
<dd>{character}</dd>
|
||||
{/if}
|
||||
|
||||
{#if additionalCharacters}
|
||||
{#each additionalCharacters as c, i (c.selection)}
|
||||
<dt><i>person</i><i>star</i></dt>
|
||||
<dd>
|
||||
<AdditionalCharacter
|
||||
on:change={changeCharacter(i)}
|
||||
selection={c.selection}
|
||||
choices={minusSelected(
|
||||
status === 'ongoing' && wave == 1
|
||||
? c.choices
|
||||
: [],
|
||||
)}
|
||||
/>
|
||||
</dd>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<dt>difficulty</dt>
|
||||
<dd>
|
||||
<div class="field ">
|
||||
<input
|
||||
type="number"
|
||||
step="0.5"
|
||||
value={difficulty}
|
||||
on:change={({ target: { value } }) =>
|
||||
event.setBattleDifficulty(
|
||||
params.battleId,
|
||||
value,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<Stars
|
||||
on:change={(e) => (difficulty = e.target.value)}
|
||||
config={{
|
||||
readOnly: false, //status !== 'preparation',
|
||||
countStars: 5,
|
||||
score: difficulty,
|
||||
showScore: false,
|
||||
starConfig: {
|
||||
size: 15,
|
||||
fillColor: '#F9ED4F',
|
||||
strokeColor: '#BB8511',
|
||||
unfilledColor: '#FFF',
|
||||
strokeUnfilledColor: '#000',
|
||||
},
|
||||
range: { min: 0, max: 5, step: 0.5 },
|
||||
}}
|
||||
/>
|
||||
{difficulty}
|
||||
-->
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<div class="space" />
|
||||
|
||||
<div class="actions">
|
||||
{#if status === 'upcoming'}
|
||||
<button>Start battle</button>
|
||||
{:else if status === 'ongoing'}
|
||||
<button on:click={battleVerdict('won')}
|
||||
>Battle won</button
|
||||
>
|
||||
<button
|
||||
class="tertiary"
|
||||
on:click={battleVerdict('lost')}>Battle lost</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="space" />
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
<footer class="absolute center bottom">
|
||||
<nav>
|
||||
<a
|
||||
on:click={() =>
|
||||
transition.goLeft(
|
||||
`#/campaign/${$activeCampaign?._id}` +
|
||||
(battle?.id == 1
|
||||
? ''
|
||||
: `/battle/${parseInt(battle?.id) - 1}`),
|
||||
)}
|
||||
>
|
||||
<button class="circle transparent">
|
||||
<i>arrow_backward</i>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<a href={`#/campaign/${$activeCampaign?._id}`}>
|
||||
<button class="circle transparent">
|
||||
<i>arrow_upward</i>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<button
|
||||
on:click={() =>
|
||||
transition.goRight(
|
||||
`/campaign/${$activeCampaign?._id}/battle/${
|
||||
battle?.id + 1
|
||||
}`,
|
||||
)}
|
||||
disabled={battles?.length <= battle?.id}
|
||||
class="circle transparent"
|
||||
>
|
||||
<i>arrow_forward</i>
|
||||
</button>
|
||||
</nav>
|
||||
</footer>
|
||||
</article>
|
||||
{/if}
|
||||
|
||||
<script>
|
||||
import { push } from 'svelte-spa-router';
|
||||
import * as R from 'remeda';
|
||||
import { getContext } from 'svelte';
|
||||
import AdditionalCharacter from './Battle/AdditionalCharacter.svelte';
|
||||
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
export let params;
|
||||
|
||||
export let api = getContext('api');
|
||||
export let activeCampaign = api?.activeCampaign;
|
||||
export let event = api?.event;
|
||||
|
||||
event?.setActiveCampaign(params.campaignId);
|
||||
|
||||
$: battles = $activeCampaign?.battles ?? [];
|
||||
$: battle = battles && battles[params.battleId - 1];
|
||||
$: scenario = battle?.scenario;
|
||||
$: status = battle?.status;
|
||||
$: wave = battle?.wave;
|
||||
$: difficulty = battle?.difficulty ?? 0;
|
||||
$: character = battle?.character;
|
||||
$: city = battle?.city;
|
||||
$: additionalCharacters = battle?.additionalCharacters;
|
||||
|
||||
$: console.log(additionalCharacters);
|
||||
|
||||
$: minusSelected =
|
||||
additionalCharacters &&
|
||||
R.difference(additionalCharacters.map(R.prop('selection')).flat());
|
||||
|
||||
$: chapter = 1 + parseInt((params.battleId - 1) / 2);
|
||||
$: chapterBattle =
|
||||
params.battleId >= 7
|
||||
? params.battleId - 6
|
||||
: 1 + ((params.battleId - 1) % 2);
|
||||
|
||||
const battleVerdict = (verdict) => () => {
|
||||
event?.setBattleVerdict(params.battleId, verdict);
|
||||
|
||||
if (verdict === 'lost') {
|
||||
if (chapter === 4) {
|
||||
push('/campaign/' + $activeCampaign._id);
|
||||
} else if (wave === 2) {
|
||||
push(
|
||||
'/campaign/' +
|
||||
$activeCampaign._id +
|
||||
'/battle/' +
|
||||
(parseInt(params.battleId) + 1),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (chapter === 4) {
|
||||
push('/campaign/' + $activeCampaign._id);
|
||||
} else {
|
||||
push(
|
||||
'/campaign/' +
|
||||
$activeCampaign._id +
|
||||
'/battle/' +
|
||||
(parseInt(params.battleId) + 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// $: event.setBattleDifficulty(params.battleId, difficulty);
|
||||
|
||||
const changeCharacter =
|
||||
(index) =>
|
||||
({ target: { value } }) => {
|
||||
event?.setCharacter(params?.battleId, index, value);
|
||||
};
|
||||
|
||||
const changeCity = ({ target: { value } }) => {
|
||||
event?.setCity(params.battleId, value);
|
||||
};
|
||||
|
||||
const transition = getContext('transition');
|
||||
let flyParams = { x: 200, duration: 0 };
|
||||
$: if ($transition) {
|
||||
flyParams.duration = 200;
|
||||
flyParams.x = $transition === 'right' ? 200 : -200;
|
||||
transition.set('');
|
||||
} else {
|
||||
flyParams.duration = 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
article {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
}
|
||||
h5 {
|
||||
flex: 1;
|
||||
}
|
||||
h6 {
|
||||
display: flex;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
h6 .status {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
padding-right: 1em;
|
||||
}
|
||||
dl {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 8em auto;
|
||||
grid-row-gap: 0.75em;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
dt {
|
||||
text-align: right;
|
||||
padding-right: 2em;
|
||||
}
|
||||
dd {
|
||||
display: flex;
|
||||
}
|
||||
article :global(.stars-container) {
|
||||
justify-content: left !important;
|
||||
margin-right: 1em;
|
||||
}
|
||||
input {
|
||||
width: 5em;
|
||||
}
|
||||
footer {
|
||||
width: 100%;
|
||||
padding: 0px 1em;
|
||||
}
|
||||
footer nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
nav h5 {
|
||||
padding-right: 1em;
|
||||
}
|
||||
header nav {
|
||||
max-width: 100%;
|
||||
}
|
||||
.actions {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,28 @@
|
|||
{#if choices.length === 0}
|
||||
{selection}
|
||||
{:else}
|
||||
<div class="field suffix">
|
||||
<select on:change>
|
||||
<option>{selection}</option>
|
||||
{#each choices as c (c)}
|
||||
<option>{c}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<i>arrow_drop_down</i>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<script>
|
||||
export let selection;
|
||||
export let choices;
|
||||
$: console.log(choices);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
select {
|
||||
/*width: 90%;*/
|
||||
}
|
||||
.field {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,50 @@
|
|||
<Beer />
|
||||
<Hst.Story>
|
||||
<Battle {status} params={{ battleId: 3 }} {api} />
|
||||
|
||||
<svelte:fragment slot="controls">
|
||||
<Hst.Select
|
||||
bind:value={status}
|
||||
title="status"
|
||||
options={Object.fromEntries(statuses.map((s) => [s, s]))}
|
||||
/>
|
||||
<Hst.Number bind:value={wave} title="wave" />
|
||||
</svelte:fragment>
|
||||
</Hst.Story>
|
||||
|
||||
<script>
|
||||
/** @type any */
|
||||
export let Hst;
|
||||
import { logEvent } from 'histoire/client';
|
||||
import { readable } from 'svelte/store';
|
||||
import Beer from '../Beer.svelte';
|
||||
import Battle from '../Battle.svelte';
|
||||
|
||||
let status = 'upcoming';
|
||||
let wave = '1';
|
||||
|
||||
const api = {
|
||||
event: {
|
||||
setCharacter: (...e) => logEvent('setCharacter', e),
|
||||
setActiveCampaign: () => {},
|
||||
},
|
||||
activeCampaign: readable({
|
||||
battles: [
|
||||
null,
|
||||
null,
|
||||
{
|
||||
wave,
|
||||
status: 'ongoing',
|
||||
scenario: 's',
|
||||
city: 'c',
|
||||
character: 'bob',
|
||||
additionalCharacters: [
|
||||
{ choices: ['one', 'two'], selection: 'one' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
let statuses = ['upcoming', 'prep', 'ongoing', 'won', 'lost'];
|
||||
</script>
|
|
@ -0,0 +1,53 @@
|
|||
<Beer />
|
||||
<Hst.Story>
|
||||
<Battle {status} params={{ battleId: 5 }} {api} />
|
||||
|
||||
<svelte:fragment slot="controls">
|
||||
<Hst.Select
|
||||
bind:value={status}
|
||||
title="status"
|
||||
options={Object.fromEntries(statuses.map((s) => [s, s]))}
|
||||
/>
|
||||
<Hst.Number bind:value={wave} title="wave" />
|
||||
</svelte:fragment>
|
||||
</Hst.Story>
|
||||
|
||||
<script>
|
||||
/** @type any */
|
||||
export let Hst;
|
||||
import { logEvent } from 'histoire/client';
|
||||
import { readable } from 'svelte/store';
|
||||
import Beer from '../Beer.svelte';
|
||||
import Battle from '../Battle.svelte';
|
||||
|
||||
let status = 'upcoming';
|
||||
let wave = '1';
|
||||
|
||||
const api = {
|
||||
event: {
|
||||
setCharacter: (...e) => logEvent('setCharacter', e),
|
||||
setActiveCampaign: () => {},
|
||||
},
|
||||
activeCampaign: readable({
|
||||
battles: [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
wave,
|
||||
status: 'ongoing',
|
||||
scenario: 's',
|
||||
city: 'c',
|
||||
character: 'bob',
|
||||
additionalCharacters: [
|
||||
{ choices: ['one', 'two'], selection: 'one' },
|
||||
{ choices: ['three', 'four'], selection: 'three' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
let statuses = ['upcoming', 'prep', 'ongoing', 'won', 'lost'];
|
||||
</script>
|
|
@ -0,0 +1,59 @@
|
|||
<Beer />
|
||||
<Hst.Story>
|
||||
<Battle {status} params={{ battleId: 7 }} {api} />
|
||||
|
||||
<svelte:fragment slot="controls">
|
||||
<Hst.Select
|
||||
bind:value={status}
|
||||
title="status"
|
||||
options={Object.fromEntries(statuses.map((s) => [s, s]))}
|
||||
/>
|
||||
<Hst.Number bind:value={wave} title="wave" />
|
||||
</svelte:fragment>
|
||||
</Hst.Story>
|
||||
|
||||
<script>
|
||||
/** @type any */
|
||||
export let Hst;
|
||||
import { logEvent } from 'histoire/client';
|
||||
import { readable } from 'svelte/store';
|
||||
import Beer from '../Beer.svelte';
|
||||
import Battle from '../Battle.svelte';
|
||||
|
||||
let status = 'upcoming';
|
||||
let wave = '1';
|
||||
|
||||
const choices = ['one', 'two', 'three', 'four'];
|
||||
const api = {
|
||||
event: {
|
||||
setCharacter: (...e) => logEvent('setCharacter', e),
|
||||
setActiveCampaign: () => {},
|
||||
},
|
||||
activeCampaign: readable({
|
||||
battles: [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{
|
||||
status: 'ongoing',
|
||||
wave: 1,
|
||||
scenario: 'The Last Battle',
|
||||
city: {
|
||||
choices: ['a', 'b'],
|
||||
selection: 'a',
|
||||
},
|
||||
additionalCharacters: [
|
||||
{ choices, selection: 'one' },
|
||||
{ choices, selection: 'three' },
|
||||
{ choices, selection: 'two' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
let statuses = ['upcoming', 'prep', 'ongoing', 'won', 'lost'];
|
||||
</script>
|
|
@ -0,0 +1,31 @@
|
|||
import { test, expect, afterEach } from 'vitest';
|
||||
import { render, cleanup } from '@testing-library/svelte';
|
||||
import Battle from '../Battle.svelte';
|
||||
import { readable } from 'svelte/store';
|
||||
|
||||
const activeCampaign = readable({
|
||||
battles: Array(12)
|
||||
.fill(null)
|
||||
.map((_, i) => ({
|
||||
id: i + 1,
|
||||
})),
|
||||
});
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
test.each([
|
||||
[2, 2],
|
||||
[3, 1],
|
||||
[7, 1],
|
||||
[8, 2],
|
||||
[9, 3],
|
||||
])('chapter battle (%i,%i))', (battleId, expected) => {
|
||||
const { container } = render(Battle, {
|
||||
params: { battleId },
|
||||
activeCampaign,
|
||||
});
|
||||
|
||||
expect(container.querySelector('.battleNbr').innerHTML).toMatch(
|
||||
'battle ' + expected,
|
||||
);
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
<script>
|
||||
import "beercss/dist/cdn/beer.min.css";
|
||||
import "beercss/dist/cdn/material-symbols-outlined.woff2";
|
||||
import "beercss/dist/cdn/roboto-flex-cyrillic-ext.woff2";
|
||||
import "beercss/dist/cdn/roboto-flex-cyrillic.woff2";
|
||||
import "beercss/dist/cdn/roboto-flex-greek.woff2";
|
||||
import "beercss/dist/cdn/roboto-flex-latin-ext.woff2";
|
||||
import "beercss/dist/cdn/roboto-flex-latin.woff2";
|
||||
import "beercss/dist/cdn/roboto-flex-vietnamese.woff2";
|
||||
</script>
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
<article class="scroll">
|
||||
<header class="fixed fill">
|
||||
<nav>
|
||||
<a href="#/">
|
||||
<button class="circle transparent">
|
||||
<i>menu</i>
|
||||
</button></a
|
||||
>
|
||||
<h5 class="max right-align">
|
||||
{#if $activeCampaign}
|
||||
{$activeCampaign.name}
|
||||
{/if}
|
||||
</h5>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{#key params.campaignId}
|
||||
<div in:fly={flyParams} class="body">
|
||||
{#if $activeCampaign}
|
||||
{#if status === 'won'}
|
||||
<h4 class="blue-text">
|
||||
{status.toUpperCase()}
|
||||
|
||||
{score}
|
||||
<i>star</i>
|
||||
</h4>
|
||||
{:else if status === 'lost'}
|
||||
<h4 class="red-text">
|
||||
{status.toUpperCase()}
|
||||
</h4>
|
||||
{:else}
|
||||
<div class="score">
|
||||
score {#if status !== 'ongoing'}so far{/if}: {score}
|
||||
<i>star</i>
|
||||
</div>
|
||||
{/if}
|
||||
<h6>Chapter 1</h6>
|
||||
<Battle
|
||||
{...battles[0]}
|
||||
{campaignId}
|
||||
chapter={1}
|
||||
chapterBattle={1}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
<Battle
|
||||
{...battles[1] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={1}
|
||||
chapterBattle={2}
|
||||
/>
|
||||
<div class="medium-divider" {campaignId} />
|
||||
<h6>Chapter 2</h6>
|
||||
<Battle
|
||||
{...battles[2] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={2}
|
||||
chapterBattle={1}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
<Battle
|
||||
{...battles[3] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={2}
|
||||
chapterBattle={2}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
<h6>Chapter 3</h6>
|
||||
<Battle
|
||||
{...battles[4] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={3}
|
||||
chapterBattle={1}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
<Battle
|
||||
{...battles[5] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={3}
|
||||
chapterBattle={2}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
<h6>Chapter 4</h6>
|
||||
<Battle
|
||||
{...battles[6] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={4}
|
||||
chapterBattle={1}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
<Battle
|
||||
{...battles[7] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={4}
|
||||
chapterBattle={2}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
<Battle
|
||||
{...battles[8] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={4}
|
||||
chapterBattle={3}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
<Battle
|
||||
{...battles[9] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={4}
|
||||
chapterBattle={4}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
<Battle
|
||||
{...battles[10] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={4}
|
||||
chapterBattle={5}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
<Battle
|
||||
{...battles[11] ?? { id: 'notYet' }}
|
||||
{campaignId}
|
||||
chapter={4}
|
||||
chapterBattle={6}
|
||||
/>
|
||||
<div class="medium-divider" />
|
||||
{/if}
|
||||
</div>{/key}
|
||||
<footer class="fixed">
|
||||
<nav>
|
||||
<a href={`#/campaign/${$activeCampaign?._id}/battle/1`}>
|
||||
<button class="circle transparent">
|
||||
<i>arrow_forward</i>
|
||||
</button>
|
||||
</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<script>
|
||||
import * as R from 'remeda';
|
||||
import Battle from './Campaign/Battle.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
export let params;
|
||||
|
||||
const api = getContext('api');
|
||||
|
||||
api.event.setActiveCampaign(params.campaignId);
|
||||
|
||||
const { activeCampaign } = getContext('api');
|
||||
|
||||
$: battles = $activeCampaign?.battles ?? [];
|
||||
|
||||
const campaignId = params.campaignId;
|
||||
|
||||
$: score = $activeCampaign?.score;
|
||||
$: status = $activeCampaign?.status;
|
||||
|
||||
$: byChapter = R.pipe(
|
||||
$activeCampaign?.battles ?? [],
|
||||
R.groupBy(({ id }) => R.clamp(1 + parseInt(id / 2), { max: 4 })),
|
||||
);
|
||||
|
||||
let flyParams = { x: 200, duration: 200 };
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.score {
|
||||
text-align: right;
|
||||
}
|
||||
article {
|
||||
height: 100vh;
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
footer nav {
|
||||
text-align: right;
|
||||
}
|
||||
nav h5 {
|
||||
padding-right: 1em;
|
||||
}
|
||||
h4 {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,48 @@
|
|||
<div class="row">
|
||||
<p>
|
||||
{#if id == 'notYet'}
|
||||
???
|
||||
{:else}
|
||||
<a href={`#/campaign/${campaignId}/battle/${id}`}>
|
||||
Battle of {typeof city === 'string' ? city : city.selection}</a
|
||||
>
|
||||
<div>
|
||||
{#if status === 'ongoing' && wave == 2}
|
||||
<i class="orange-text">mood_bad</i>
|
||||
{:else if status === 'ongoing' && wave == 1}
|
||||
<i style="color: blue">sentiment_neutral</i>
|
||||
{:else if status === 'lost'}
|
||||
<i style="color: red">sentiment_very_dissatisfied</i>
|
||||
{:else if status === 'won'}
|
||||
<i style="color: green">mood</i>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="difficulty">{difficulty} <i>star</i></div>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
export let city = '';
|
||||
export let status = '';
|
||||
export let id = 0;
|
||||
export let wave = 1;
|
||||
export let campaignId;
|
||||
export let chapter;
|
||||
export let chapterBattle;
|
||||
export let difficulty = 0;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.row p {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.row p a {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
}
|
||||
.difficulty {
|
||||
margin-left: 1em;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,25 @@
|
|||
<Beer />
|
||||
|
||||
<Hst.Story>
|
||||
<Campaigns {campaigns} />
|
||||
</Hst.Story>
|
||||
|
||||
<script>
|
||||
import { setContext } from 'svelte';
|
||||
|
||||
export let Hst;
|
||||
import Beer from './Beer.svelte';
|
||||
import Campaigns from './Campaigns.svelte';
|
||||
import { genApi } from '$lib/store/api.js';
|
||||
|
||||
global.__SVELTEKIT_APP_VERSION_POLL_INTERVAL__ = 1000;
|
||||
|
||||
const campaigns = [{ name: 'Earth 613' }];
|
||||
|
||||
setContext(
|
||||
'api',
|
||||
genApi({
|
||||
local: campaigns,
|
||||
}),
|
||||
);
|
||||
</script>
|
|
@ -0,0 +1,125 @@
|
|||
<header class="fixed fill">
|
||||
<nav>
|
||||
<h5 class="max center-align">Campaigns</h5>
|
||||
<button class="circle small" on:click={newCampaign}>
|
||||
<i>add</i>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<article>
|
||||
{#each $campaigns as campaign (campaign._id)}
|
||||
<div class="row">
|
||||
<i class="light-green-text">south_america</i>
|
||||
<div class="max">
|
||||
<h6>
|
||||
<a href={`#/campaign/${campaign._id}`}>
|
||||
{campaign.name}
|
||||
</a>
|
||||
</h6>
|
||||
<p>
|
||||
{#if campaign.battles}
|
||||
<a href={`#/campaign/${campaign._id}`}>
|
||||
chapter {currentChapter(campaign)}, battle of {currentCity(
|
||||
campaign,
|
||||
)}.
|
||||
</a>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<button on:click={deleteCampaign(campaign)} class="none"
|
||||
>Delete</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
<footer class="bottom absolute center">
|
||||
fan-made campaign manager for <a
|
||||
href="https://czechgames.com/en/under-falling-skies/"
|
||||
>Under Falling Skies</a
|
||||
><br /> written by
|
||||
<a href="mailto:yanick@sky.babyl.ca">Yanick Champoux</a>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
{#if showNewCampaign}
|
||||
<div class="modal active">
|
||||
<form>
|
||||
<h5>New campaign</h5>
|
||||
<div>
|
||||
<div class="field border">
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
bind:value={newCampaignName}
|
||||
placeholder="Campaign name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="right-align">
|
||||
<button
|
||||
class="border"
|
||||
on:click={() => (showNewCampaign = false)}>Cancel</button
|
||||
>
|
||||
<button on:click={saveNewCampaign}>Create</button>
|
||||
</nav>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<script>
|
||||
import * as R from 'remeda';
|
||||
import { getContext } from 'svelte';
|
||||
// import { goto } from '$app/navigation';
|
||||
const goto = () => {};
|
||||
|
||||
export let api = getContext('api');
|
||||
export let campaigns = api?.campaigns;
|
||||
|
||||
let showNewCampaign = false;
|
||||
let newCampaignName = '';
|
||||
|
||||
const currentChapter = ({ battles }) =>
|
||||
R.clamp(1 + parseInt(battles?.length / 2), { max: 4 });
|
||||
const currentCity = ({ battles }) => {
|
||||
const c = R.last(battles)?.city;
|
||||
return typeof c === 'string' ? c : c?.selection;
|
||||
};
|
||||
|
||||
async function deleteCampaign({ name, _id }) {
|
||||
if (!window.confirm(`delete campaign ${name}?`)) return;
|
||||
api.event.deleteCampaign(_id);
|
||||
}
|
||||
|
||||
const newCampaign = () => {
|
||||
showNewCampaign = true;
|
||||
};
|
||||
|
||||
const saveNewCampaign = () => {
|
||||
console.log('sent');
|
||||
api.event.addCampaign(newCampaignName);
|
||||
newCampaignName = '';
|
||||
showNewCampaign = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
width: 100%;
|
||||
}
|
||||
header {
|
||||
display: flex;
|
||||
}
|
||||
h5 {
|
||||
flex: 1;
|
||||
}
|
||||
article {
|
||||
height: 90vh;
|
||||
}
|
||||
footer {
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
footer a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,199 @@
|
|||
import * as R from 'remeda';
|
||||
import PouchDB from 'pouchdb';
|
||||
import { writable, derived, get } from 'svelte/store';
|
||||
import { genNextBattle } from './genNextBattle.js';
|
||||
import u from '@yanick/updeep-remeda';
|
||||
|
||||
const seedCampaign = {
|
||||
status: 'ongoing', // win, lost
|
||||
score: 0,
|
||||
battles: [],
|
||||
};
|
||||
|
||||
function campaignStatus(campaign) {
|
||||
if (campaign.battles.length <= 6) return 'ongoing';
|
||||
|
||||
if (R.last(campaign.battles).status === 'won') return 'won';
|
||||
|
||||
if (
|
||||
campaign.battles.length - 6 >=
|
||||
campaign.battles.slice(0, 6).filter(({ status }) => status === 'won')
|
||||
)
|
||||
return 'lost';
|
||||
|
||||
return 'ongoing';
|
||||
}
|
||||
|
||||
function calculateScore(campaign) {
|
||||
const values = [
|
||||
-campaign.battles.slice(0, 6).filter(({ wave }) => wave === 2).length,
|
||||
-campaign.battles.slice(6).filter(({ status }) => status === 'lost')
|
||||
.length,
|
||||
...campaign.battles
|
||||
.map(({ difficulty }) => parseInt(difficulty))
|
||||
.map((x) => (Number.isNaN(x) ? 0 : x)),
|
||||
];
|
||||
console.log(values);
|
||||
return values.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
|
||||
export function updateBattle(campaign, battleId, status) {
|
||||
const battle = campaign.battles[battleId - 1];
|
||||
if (status === 'lost' && battle.wave == 1 && battle.id < 7) {
|
||||
campaign = u.updateIn(campaign, `battles.${battleId - 1}`, {
|
||||
wave: 2,
|
||||
});
|
||||
campaign = u.updateIn(campaign, 'score', calculateScore(campaign));
|
||||
return campaign;
|
||||
}
|
||||
|
||||
campaign = u.updateIn(campaign, `battles.${battleId - 1}`, {
|
||||
status,
|
||||
});
|
||||
|
||||
campaign = u.updateIn(campaign, 'score', calculateScore(campaign));
|
||||
console.log(campaign);
|
||||
|
||||
campaign = u(campaign, {
|
||||
status: campaignStatus(campaign),
|
||||
});
|
||||
if (campaign.status === 'ongoing') {
|
||||
campaign = u(campaign, {
|
||||
battles: (battles) => [
|
||||
...battles,
|
||||
u(genNextBattle(battles), {
|
||||
id: battles.length + 1,
|
||||
}),
|
||||
],
|
||||
});
|
||||
console.log('updated', campaign);
|
||||
}
|
||||
return campaign;
|
||||
}
|
||||
|
||||
export function genApi(options = {}) {
|
||||
// if (options.local) MyPouch.plugin(pouchMem);
|
||||
if (options.pouch) options.pouch(PouchDB);
|
||||
|
||||
const pouchdb = new PouchDB(
|
||||
'Campaigns',
|
||||
options.local
|
||||
? {
|
||||
adapter: 'memory',
|
||||
}
|
||||
: {},
|
||||
);
|
||||
|
||||
const campaignsCore = writable([]);
|
||||
|
||||
pouchdb
|
||||
.allDocs({ include_docs: true })
|
||||
.then((r) => r.rows.map(({ id, doc }) => [id, doc]))
|
||||
.then(Object.fromEntries)
|
||||
.then(campaignsCore.set);
|
||||
|
||||
pouchdb
|
||||
.changes({ since: 'now', live: true, include_docs: true })
|
||||
.on('change', (change) => {
|
||||
if (change.deleted) {
|
||||
campaignsCore.update(u.updateIn(change.id, u.skip));
|
||||
} else {
|
||||
campaignsCore.update(u.updateIn(change.id, change.doc));
|
||||
}
|
||||
});
|
||||
|
||||
const campaigns = derived(campaignsCore, Object.values);
|
||||
|
||||
const addCampaign = (name) =>
|
||||
pouchdb.post({
|
||||
name,
|
||||
...seedCampaign,
|
||||
battles: [{ id: 1, ...genNextBattle() }],
|
||||
});
|
||||
|
||||
const activeCampaignId = writable(0);
|
||||
|
||||
const setActiveCampaign = activeCampaignId.set;
|
||||
|
||||
const activeCampaign = derived(
|
||||
[campaignsCore, activeCampaignId],
|
||||
([$c, $id]) => $c[$id],
|
||||
);
|
||||
|
||||
const setBattleDifficulty = (battleId, difficulty) => {
|
||||
pouchdb
|
||||
.put(
|
||||
u.updateIn(get(activeCampaign), `battles.${battleId - 1}`, {
|
||||
difficulty,
|
||||
}),
|
||||
)
|
||||
.catch((e) => console.error(e));
|
||||
};
|
||||
|
||||
const setBattleVerdict = (battleId, status) => {
|
||||
const campaign = updateBattle(get(activeCampaign), battleId, status);
|
||||
pouchdb.put(campaign).catch((e) => console.error(e));
|
||||
};
|
||||
|
||||
const deleteCampaign = (_id) =>
|
||||
pouchdb.remove(get(campaigns).find(u.matches({ _id })));
|
||||
|
||||
const setCharacter = (battleId, characterId, value) => {
|
||||
const campaign = u.updateIn(
|
||||
get(activeCampaign),
|
||||
['battles', battleId, 'additionalCharacters', 'selection'],
|
||||
value,
|
||||
);
|
||||
|
||||
pouchdb.put(campaign);
|
||||
};
|
||||
|
||||
const setCity = (battleId, value) => {
|
||||
const campaign = u.updateIn(
|
||||
get(activeCampaign),
|
||||
['battles', battleId, 'city', 'selection'],
|
||||
value,
|
||||
);
|
||||
|
||||
pouchdb.put(campaign);
|
||||
};
|
||||
|
||||
return {
|
||||
campaigns,
|
||||
activeCampaign,
|
||||
event: {
|
||||
setCity,
|
||||
setCharacter,
|
||||
deleteCampaign,
|
||||
addCampaign,
|
||||
setActiveCampaign,
|
||||
setBattleVerdict,
|
||||
setBattleDifficulty,
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
const activeCampaign = writeable();
|
||||
|
||||
addCampaign: (name) =>
|
||||
storage.campaigns.add({
|
||||
name,
|
||||
...seedCampaign,
|
||||
battles: [{ id: 1, ...genNextBattle() }],
|
||||
}),
|
||||
|
||||
return {
|
||||
campaigns,
|
||||
event: {
|
||||
,
|
||||
addCampaign: (name) =>
|
||||
storage.campaigns.add({
|
||||
name,
|
||||
...seedCampaign,
|
||||
battles: [{ id: 1, ...genNextBattle() }],
|
||||
}),
|
||||
deleteCampaign: (id) => storage.campaigns.delete(id),
|
||||
},
|
||||
};
|
||||
*/
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { test, expect } from 'vitest';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { genApi } from './api.js';
|
||||
import adapter from 'pouchdb-adapter-memory';
|
||||
|
||||
const waitUntil = (store, condition) => {
|
||||
return new Promise((resolve) => {
|
||||
store.subscribe((v) => condition(v) && resolve(v));
|
||||
});
|
||||
};
|
||||
|
||||
test('create and add and remove campaigns', async () => {
|
||||
const api = genApi({
|
||||
local: true,
|
||||
pouch: (p) => p.plugin(adapter),
|
||||
});
|
||||
|
||||
let result = waitUntil(api.campaigns, (r) => r.length === 2);
|
||||
|
||||
api.event.addCampaign('C1');
|
||||
api.event.addCampaign('C2');
|
||||
|
||||
await result;
|
||||
|
||||
const [c] = get(api.campaigns);
|
||||
|
||||
expect(c).toMatchObject({
|
||||
name: 'C1',
|
||||
});
|
||||
|
||||
const r = waitUntil(api.campaigns, (r) => r.length === 1);
|
||||
|
||||
api.event.deleteCampaign(c._id);
|
||||
|
||||
await expect(r).resolves.toBeTruthy();
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
export default [
|
||||
{
|
||||
cities: ['Havana', 'Montreal', 'Mexico City', 'Rio de Janeiro'],
|
||||
characters: [
|
||||
'Clinton Harper',
|
||||
'Jackson Moss',
|
||||
'Lucia Ortego',
|
||||
'Samantha Legrand',
|
||||
],
|
||||
scenarios: [
|
||||
'Evacuation',
|
||||
'Reinforcements',
|
||||
'Battle for the sky',
|
||||
'Satellites',
|
||||
],
|
||||
},
|
||||
{
|
||||
cities: ['Johannesburg', 'Moscow', 'Cairo', 'London', 'Paris'],
|
||||
characters: [
|
||||
'Jaroslav Ruzicka',
|
||||
'Karima Almasi',
|
||||
'Shanti Aumann',
|
||||
'Pieter Bernstein',
|
||||
],
|
||||
scenarios: [
|
||||
'Command ship',
|
||||
'Reactor leak',
|
||||
'Dangerous research',
|
||||
'Repairing the base',
|
||||
],
|
||||
},
|
||||
{
|
||||
cities: ['Seoul', 'Beijing', 'Tokyo', 'Sydney', 'Singapore'],
|
||||
characters: ['Wang Lin', "Iz'ox", 'Jang Chanwook', 'Archie Bell'],
|
||||
scenarios: ['Storm', 'Contamination', 'Kamikaze', 'Saboteur'],
|
||||
},
|
||||
{
|
||||
scenarios: ['The final battle'],
|
||||
},
|
||||
];
|
|
@ -0,0 +1,142 @@
|
|||
import * as R from 'remeda';
|
||||
import chapters from './chapters.js';
|
||||
|
||||
const pickOne = (choices) => choices[parseInt(Math.random() * choices.length)];
|
||||
|
||||
function genChapter1Battle(battles) {
|
||||
const chapter = 1;
|
||||
|
||||
const scenario = pickOne(
|
||||
chapters[chapter - 1].scenarios.filter(
|
||||
(s) => !battles.map(R.prop('scenario')).includes(s),
|
||||
),
|
||||
);
|
||||
const character = pickOne(
|
||||
chapters[chapter - 1].characters.filter(
|
||||
(s) => !battles.map(R.prop('character')).includes(s),
|
||||
),
|
||||
);
|
||||
const city = pickOne(
|
||||
chapters[chapter - 1].cities.filter(
|
||||
(s) => !battles.map(R.prop('city')).includes(s),
|
||||
),
|
||||
);
|
||||
|
||||
return { scenario, character, city, status: 'ongoing', wave: 1 };
|
||||
}
|
||||
|
||||
export function genChapter2Battle(battles) {
|
||||
const chapter = 2;
|
||||
|
||||
const scenario = pickOne(
|
||||
chapters[chapter - 1].scenarios.filter(
|
||||
(s) => !battles.map(R.prop('scenario')).includes(s),
|
||||
),
|
||||
);
|
||||
const character = pickOne(
|
||||
chapters[chapter - 1].characters.filter(
|
||||
(s) => !battles.map(R.prop('character')).includes(s),
|
||||
),
|
||||
);
|
||||
const city = pickOne(
|
||||
chapters[chapter - 1].cities.filter(
|
||||
(s) => !battles.map(R.prop('city')).includes(s),
|
||||
),
|
||||
);
|
||||
|
||||
let choices = battles.slice(0, 2).map(R.prop('character'));
|
||||
if (battles.length == 3)
|
||||
choices = choices.filter(
|
||||
(n) => n !== R.last(battles).additionalCharacters[0].selection,
|
||||
);
|
||||
|
||||
return {
|
||||
scenario,
|
||||
character,
|
||||
city,
|
||||
status: 'ongoing',
|
||||
wave: 1,
|
||||
additionalCharacters: [{ choices, selection: R.first(choices) }],
|
||||
};
|
||||
}
|
||||
export function genChapter3Battle(battles) {
|
||||
const chapter = 3;
|
||||
|
||||
const scenario = pickOne(
|
||||
chapters[chapter - 1].scenarios.filter(
|
||||
(s) => !battles.map(R.prop('scenario')).includes(s),
|
||||
),
|
||||
);
|
||||
const character = pickOne(
|
||||
chapters[chapter - 1].characters.filter(
|
||||
(s) => !battles.map(R.prop('character')).includes(s),
|
||||
),
|
||||
);
|
||||
const city = pickOne(
|
||||
chapters[chapter - 1].cities.filter(
|
||||
(s) => !battles.map(R.prop('city')).includes(s),
|
||||
),
|
||||
);
|
||||
|
||||
let choices1 = battles.slice(0, 2).map(R.prop('character'));
|
||||
let choices2 = battles.slice(2, 4).map(R.prop('character'));
|
||||
if (battles.length == 5) {
|
||||
choices1 = choices1.filter(
|
||||
(n) => n !== R.last(battles).additionalCharacters[0].selection,
|
||||
);
|
||||
choices2 = choices2.filter(
|
||||
(n) => n !== R.last(battles).additionalCharacters[1].selection,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
scenario,
|
||||
character,
|
||||
city,
|
||||
status: 'ongoing',
|
||||
wave: 1,
|
||||
additionalCharacters: [choices1, choices2].map((choices) => ({
|
||||
choices,
|
||||
selection: R.first(choices),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function genChapter4Battle(battles) {
|
||||
const chapter = 4;
|
||||
|
||||
const scenario = 'The Last Battle';
|
||||
|
||||
const cities = R.difference(
|
||||
battles
|
||||
.slice(0, 6)
|
||||
.filter(({ status }) => status !== 'lost')
|
||||
.map(R.prop('city')),
|
||||
battles.slice(6).map((b) => b.city?.selection),
|
||||
);
|
||||
|
||||
const characters = battles.slice(0, 6).map(R.prop('character'));
|
||||
|
||||
return {
|
||||
wave: 1,
|
||||
scenario,
|
||||
city: { choices: cities, selection: R.first(cities) },
|
||||
status: 'ongoing',
|
||||
additionalCharacters: R.range(0, 3).map((i) => ({
|
||||
choices: characters,
|
||||
selection: characters[i],
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function genNextBattle(battles = []) {
|
||||
let chapter = R.clamp(1 + parseInt(battles.length / 2), { min: 1, max: 4 });
|
||||
|
||||
if (chapter === 1) return genChapter1Battle(battles);
|
||||
|
||||
if (chapter === 2) return genChapter2Battle(battles);
|
||||
|
||||
if (chapter === 3) return genChapter3Battle(battles);
|
||||
|
||||
return genChapter4Battle(battles);
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import { test, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
genNextBattle,
|
||||
genChapter2Battle,
|
||||
genChapter3Battle,
|
||||
genChapter4Battle,
|
||||
} from './genNextBattle.js';
|
||||
|
||||
test('generate for the first chapter', () => {
|
||||
const next = genNextBattle();
|
||||
expect(next).toHaveProperty('city');
|
||||
expect(next).toHaveProperty('status', 'ongoing');
|
||||
});
|
||||
|
||||
test('chapter 2, first battle', () => {
|
||||
const result = genChapter2Battle([
|
||||
{ character: 'one' },
|
||||
{ character: 'two' },
|
||||
]);
|
||||
|
||||
expect(result.additionalCharacters[0].selection).toEqual('one');
|
||||
expect(result.additionalCharacters[0].choices).toEqual(['one', 'two']);
|
||||
});
|
||||
|
||||
test('chapter 2, second battle', () => {
|
||||
const result = genChapter2Battle([
|
||||
{ character: 'one' },
|
||||
{ character: 'two' },
|
||||
{
|
||||
character: 'three',
|
||||
additionalCharacters: [{ selection: 'two' }],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.additionalCharacters[0].selection).toEqual('one');
|
||||
expect(result.additionalCharacters[0].choices).toEqual(['one']);
|
||||
});
|
||||
|
||||
test('chapter 3, first battle', () => {
|
||||
const result = genChapter3Battle([
|
||||
{ character: 'one' },
|
||||
{ character: 'two' },
|
||||
{ character: 'three' },
|
||||
{ character: 'four' },
|
||||
]);
|
||||
|
||||
expect(result.additionalCharacters[0].selection).toEqual('one');
|
||||
expect(result.additionalCharacters[1].selection).toEqual('three');
|
||||
});
|
||||
|
||||
test('chapter 3, second battle', () => {
|
||||
const result = genChapter3Battle([
|
||||
{ character: 'one' },
|
||||
{ character: 'two' },
|
||||
{ character: 'three' },
|
||||
{ character: 'four' },
|
||||
{
|
||||
character: 'five',
|
||||
additionalCharacters: [
|
||||
{ selection: 'one' },
|
||||
{ selection: 'three' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.additionalCharacters[0].selection).toEqual('two');
|
||||
expect(result.additionalCharacters[1].selection).toEqual('four');
|
||||
});
|
||||
|
||||
test.only('chapter 4, first battle', () => {
|
||||
const result = genChapter4Battle([
|
||||
{ character: 'one', city: 'a' },
|
||||
{ character: 'two', city: 'b', status: 'lost' },
|
||||
{ character: 'three', city: 'c' },
|
||||
{ character: 'four', city: 'd' },
|
||||
{ character: 'five', city: 'e' },
|
||||
{ character: 'six', city: 'f' },
|
||||
]);
|
||||
|
||||
expect(result.additionalCharacters[0].selection).toEqual('one');
|
||||
expect(result.additionalCharacters[1].selection).toEqual('two');
|
||||
expect(result.additionalCharacters[2].selection).toEqual('three');
|
||||
|
||||
expect(result.city.choices).not.toContain('b');
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
import { test, expect } from 'vitest';
|
||||
import { updateBattle } from './api.js';
|
||||
|
||||
test('lost, wave 1', () => {
|
||||
const result = updateBattle(
|
||||
{
|
||||
battles: [{ id: 1, wave: 1, status: 'ongoing' }],
|
||||
},
|
||||
1,
|
||||
'lost',
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('battles.0.wave', 2);
|
||||
expect(result).toHaveProperty('battles.0.status', 'ongoing');
|
||||
});
|
||||
|
||||
test('lost, wave 2', () => {
|
||||
const result = updateBattle(
|
||||
{
|
||||
battles: [{ id: 1, wave: 2, status: 'ongoing' }],
|
||||
},
|
||||
1,
|
||||
'lost',
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('battles.0.status', 'lost');
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
//import './app.css'
|
||||
import App from './lib/components/App.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app'),
|
||||
});
|
||||
|
||||
export default app;
|
|
@ -0,0 +1,21 @@
|
|||
<Beer />
|
||||
<main class="center">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<script>
|
||||
import { genApi } from '$lib/store/api.js';
|
||||
import { setContext } from 'svelte';
|
||||
import Beer from '$lib/components/Beer.svelte';
|
||||
|
||||
const api = genApi();
|
||||
|
||||
setContext('api', api);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
main {
|
||||
max-width: 720px;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,12 @@
|
|||
<Campaigns campaigns={$campaigns} />
|
||||
|
||||
<script>
|
||||
import Campaigns from '$lib/components/Campaigns.svelte';
|
||||
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
const { campaigns } = getContext('api');
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -0,0 +1,4 @@
|
|||
export function load({ params }) {
|
||||
params.campaignId = parseInt(params?.campaignId);
|
||||
return params;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<slot />
|
||||
|
||||
<script>
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
export let data;
|
||||
|
||||
const api = getContext('api');
|
||||
|
||||
api.event.setActiveCampaign(data.campaignId);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,10 @@
|
|||
import adapter from '@sveltejs/adapter-auto';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -0,0 +1,6 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('index page has expected h1', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
expect(await page.textContent('h1')).toBe('Welcome to SvelteKit');
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('vite').UserConfig} */
|
||||
const config = {
|
||||
plugins: [svelte()],
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
environment: 'jsdom',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
$lib:
|
||||
import.meta.url.replace('file://', '').replace(/[^/]+$/, '') +
|
||||
'src/lib',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
Loading…
Reference in New Issue