Merge branch 'chapter4'

This commit is contained in:
Yanick Champoux 2023-01-16 10:12:48 -05:00
commit 1919ef3850
10 changed files with 321 additions and 68 deletions

View File

@ -15,7 +15,6 @@
const api = genApi(); const api = genApi();
setContext('api', api); setContext('api', api);
console.log('api set, sucka');
const routes = { const routes = {
// Exact path // Exact path

View File

@ -1,12 +1,12 @@
<article class="scroll"> <article class="scroll">
<header class="fixed"> <header class="fixed fill">
<nav> <nav>
<a href="#/"> <a href="#/">
<button class="circle transparent"> <button class="circle transparent">
<i>menu</i> <i>menu</i>
</button></a </button></a
> >
<h5 class="max center-align"> <h5 class="max right-align">
{#if $activeCampaign} {#if $activeCampaign}
<a href={`#/campaign/${$activeCampaign._id}`}> <a href={`#/campaign/${$activeCampaign._id}`}>
{$activeCampaign.name} {$activeCampaign.name}
@ -16,25 +16,42 @@
</nav> </nav>
</header> </header>
<h6> <h6>
Chapter {chapter}, <span
<span class="battleNbr">battle {chapterBattle}</span> >Chapter {chapter},
-- {status} <span class="battleNbr">battle {chapterBattle}</span></span
>
<span class="status">{status}</span>
</h6> </h6>
<dl> <dl>
{#if status === 'ongoing'} {#if status === 'ongoing' && chapter !== 4}
<dt>wave</dt> <dt>wave</dt>
<dd>{wave === 2 ? 'second' : 'first'}</dd> <dd>{wave === 2 ? 'second' : 'first'}</dd>
{/if} {/if}
<dt>city</dt> <dt><i>home</i></dt>
<dd>{city}</dd> <dd>
<dt>scenario</dt> {#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> <dd>{scenario}</dd>
<dt>character</dt>
<dd>{character}</dd> {#if character}
<dt><i>person</i></dt>
<dd>{character}</dd>
{/if}
{#if additionalCharacters} {#if additionalCharacters}
{#each additionalCharacters as c, i (c.selection)} {#each additionalCharacters as c, i (c.selection)}
<dt>add. character</dt> <dt><i>person</i><i>star</i></dt>
<dd> <dd>
<AdditionalCharacter <AdditionalCharacter
on:change={changeCharacter(i)} on:change={changeCharacter(i)}
@ -49,7 +66,7 @@
<dt>difficulty</dt> <dt>difficulty</dt>
<dd> <dd>
<div class="field border"> <div class="field ">
<input <input
type="number" type="number"
step="0.5" step="0.5"
@ -82,12 +99,16 @@
</dd> </dd>
</dl> </dl>
{#if status === 'upcoming'} <div class="actions">
<button>Start battle</button> {#if status === 'upcoming'}
{:else if status === 'ongoing'} <button>Start battle</button>
<button on:click={battleVerdict('won')}>Battle won</button> {:else if status === 'ongoing'}
<button on:click={battleVerdict('lost')}>Battle lost</button> <button on:click={battleVerdict('won')}>Battle won</button>
{/if} <button class="tertiary" on:click={battleVerdict('lost')}
>Battle lost</button
>
{/if}
</div>
<footer class="fixed"> <footer class="fixed">
<nav> <nav>
@ -159,24 +180,30 @@
const changeCharacter = const changeCharacter =
(index) => (index) =>
({ target: { value } }) => { ({ target: { value } }) => {
event?.setCharacter(params.battleId, index, value); event?.setCharacter(params?.battleId, index, value);
}; };
const changeCity = ({ target: { value } }) => {
event?.setCity(params.battleId, value);
};
</script> </script>
<style> <style>
article { article {
width: 500px; height: 100vh;
} min-height: 100vh;
header {
display: flex;
align-items: baseline;
} }
h5 { h5 {
flex: 1; flex: 1;
} }
h6 { h6 {
font-style: italic; display: flex;
font-size: 1.25rem; margin-bottom: 1em;
}
h6 .status {
flex: 1;
text-align: right;
padding-right: 1em;
} }
dl { dl {
width: 100%; width: 100%;
@ -203,4 +230,13 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
nav h5 {
padding-right: 1em;
}
header nav {
max-width: 100%;
}
.actions {
text-align: center;
}
</style> </style>

View File

@ -1,12 +1,15 @@
{#if choices.length === 0} {#if choices.length === 0}
{selection} {selection}
{:else} {:else}
<select on:change> <div class="field suffix">
<option>{selection}</option> <select on:change>
{#each choices as c (c)} <option>{selection}</option>
<option>{c}</option> {#each choices as c (c)}
{/each} <option>{c}</option>
</select> {/each}
</select>
<i>arrow_drop_down</i>
</div>
{/if} {/if}
<script> <script>
@ -14,3 +17,12 @@
export let choices; export let choices;
$: console.log(choices); $: console.log(choices);
</script> </script>
<style>
select {
/*width: 90%;*/
}
.field {
margin-bottom: 0px;
}
</style>

View File

@ -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>

View File

@ -1,12 +1,12 @@
<article class="scroll"> <article class="scroll">
<header class="fixed"> <header class="fixed fill">
<nav> <nav>
<a href="#/"> <a href="#/">
<button class="circle transparent"> <button class="circle transparent">
<i>menu</i> <i>menu</i>
</button></a </button></a
> >
<h5 class="max center-align"> <h5 class="max right-align">
{#if $activeCampaign} {#if $activeCampaign}
{$activeCampaign.name} {$activeCampaign.name}
{/if} {/if}
@ -15,6 +15,14 @@
</header> </header>
{#if $activeCampaign} {#if $activeCampaign}
{#if status !== 'ongoing'}
<h6>{status}</h6>{/if}
<div class="score">
score {#if status !== 'ongoing'}so far{/if}: {score}
<i>star</i>
</div>
<h6>Chapter 1</h6> <h6>Chapter 1</h6>
<Battle {...battles[0]} {campaignId} chapter={1} chapterBattle={1} /> <Battle {...battles[0]} {campaignId} chapter={1} chapterBattle={1} />
<div class="medium-divider" /> <div class="medium-divider" />
@ -101,7 +109,7 @@
{/if} {/if}
<footer class="fixed"> <footer class="fixed">
<nav> <nav>
<a href={`#/campaign/${$activeCampaign._id}/battle/1`}> <a href={`#/campaign/${$activeCampaign?._id}/battle/1`}>
<button class="circle transparent"> <button class="circle transparent">
<i>arrow_forward</i> <i>arrow_forward</i>
</button> </button>
@ -128,6 +136,9 @@
const campaignId = params.campaignId; const campaignId = params.campaignId;
$: score = $activeCampaign?.score;
$: status = $activeCampaign?.status;
$: byChapter = R.pipe( $: byChapter = R.pipe(
$activeCampaign?.battles ?? [], $activeCampaign?.battles ?? [],
R.groupBy(({ id }) => R.clamp(1 + parseInt(id / 2), { max: 4 })), R.groupBy(({ id }) => R.clamp(1 + parseInt(id / 2), { max: 4 })),
@ -137,6 +148,9 @@
</script> </script>
<style> <style>
.score {
text-align: right;
}
article { article {
height: 100vh; height: 100vh;
} }
@ -147,4 +161,7 @@
footer nav { footer nav {
text-align: right; text-align: right;
} }
nav h5 {
padding-right: 1em;
}
</style> </style>

View File

@ -4,8 +4,20 @@
??? ???
{:else} {:else}
<a href={`#/campaign/${campaignId}/battle/${id}`}> <a href={`#/campaign/${campaignId}/battle/${id}`}>
Battle of {city}</a Battle of {typeof city === 'string' ? city : city.selection}</a
><span>, {status}</span> >
<div>
{#if status === 'ongoing' && wave == 2}
<i style="color: yellow">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} {/if}
</p> </p>
</div> </div>
@ -14,10 +26,23 @@
export let city = ''; export let city = '';
export let status = ''; export let status = '';
export let id = 0; export let id = 0;
export let wave = 1;
export let campaignId; export let campaignId;
export let chapter; export let chapter;
export let chapterBattle; export let chapterBattle;
export let difficulty = 0;
</script> </script>
<style> <style>
.row p {
display: flex;
width: 100%;
}
.row p a {
display: inline-block;
flex: 1;
}
.difficulty {
margin-left: 1em;
}
</style> </style>

View File

@ -1,4 +1,4 @@
<header> <header class="fixed fill">
<nav> <nav>
<h5 class="max center-align">Campaigns</h5> <h5 class="max center-align">Campaigns</h5>
<button class="circle small" on:click={newCampaign}> <button class="circle small" on:click={newCampaign}>
@ -19,9 +19,11 @@
</h6> </h6>
<p> <p>
{#if campaign.battles} {#if campaign.battles}
chapter {currentChapter(campaign)}, battle of {currentCity( <a href={`#/campaign/${campaign._id}`}>
campaign, chapter {currentChapter(campaign)}, battle of {currentCity(
)}. campaign,
)}.
</a>
{/if} {/if}
</p> </p>
</div> </div>
@ -32,24 +34,30 @@
{/each} {/each}
</article> </article>
<div class="modal" class:active={showNewCampaign}> {#if showNewCampaign}
<h5>New campaign</h5> <div class="modal active">
<div> <form>
<div class="field border"> <h5>New campaign</h5>
<input <div>
type="text" <div class="field border">
bind:value={newCampaignName} <input
placeholder="Campaign name" autofocus
/> type="text"
</div> 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> </div>
<nav class="right-align"> {/if}
<button class="border" on:click={() => (showNewCampaign = false)}
>Cancel</button
>
<button on:click={saveNewCampaign}>Create</button>
</nav>
</div>
<script> <script>
import * as R from 'remeda'; import * as R from 'remeda';
@ -65,7 +73,10 @@
const currentChapter = ({ battles }) => const currentChapter = ({ battles }) =>
R.clamp(1 + parseInt(battles?.length / 2), { max: 4 }); R.clamp(1 + parseInt(battles?.length / 2), { max: 4 });
const currentCity = ({ battles }) => R.last(battles).city; const currentCity = ({ battles }) => {
const c = R.last(battles).city;
return typeof c === 'string' ? c : c.selection;
};
async function deleteCampaign({ name, _id }) { async function deleteCampaign({ name, _id }) {
if (!window.confirm(`delete campaign ${name}?`)) return; if (!window.confirm(`delete campaign ${name}?`)) return;
@ -77,6 +88,7 @@
}; };
const saveNewCampaign = () => { const saveNewCampaign = () => {
console.log('sent');
api.event.addCampaign(newCampaignName); api.event.addCampaign(newCampaignName);
newCampaignName = ''; newCampaignName = '';
showNewCampaign = false; showNewCampaign = false;

View File

@ -1,15 +1,41 @@
import * as R from 'remeda';
import PouchDB from 'pouchdb'; import PouchDB from 'pouchdb';
import { writable, derived, get } from 'svelte/store'; import { writable, derived, get } from 'svelte/store';
import { genNextBattle } from './genNextBattle.js'; import { genNextBattle } from './genNextBattle.js';
import u from '@yanick/updeep-remeda'; import u from '@yanick/updeep-remeda';
const seedCampaign = { const seedCampaign = {
status: 'ongoing', // win, lost
score: 0,
battles: [], 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) {
return [
// -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)),
].reduce((a, b) => a + b, 0);
}
export function updateBattle(campaign, battleId, status) { export function updateBattle(campaign, battleId, status) {
const battle = campaign.battles[battleId - 1]; const battle = campaign.battles[battleId - 1];
if (status === 'lost' && battle.wave == 1) { if (status === 'lost' && (battle.wave == 1 || battle.id < 7)) {
return u.updateIn(campaign, `battles.${battleId - 1}`, { return u.updateIn(campaign, `battles.${battleId - 1}`, {
wave: 2, wave: 2,
}); });
@ -17,14 +43,23 @@ export function updateBattle(campaign, battleId, status) {
campaign = u.updateIn(campaign, `battles.${battleId - 1}`, { campaign = u.updateIn(campaign, `battles.${battleId - 1}`, {
status, status,
}); });
campaign = u.updateIn(campaign, 'score', calculateScore(campaign));
console.log(campaign);
campaign = u(campaign, { campaign = u(campaign, {
battles: (battles) => [ status: campaignStatus(campaign),
...battles,
u(genNextBattle(battles), {
id: battles.length + 1,
}),
],
}); });
if (campaign.status === 'ongoing') {
campaign = u(campaign, {
battles: (battles) => [
...battles,
u(genNextBattle(battles), {
id: battles.length + 1,
}),
],
});
}
return campaign; return campaign;
} }
@ -105,10 +140,21 @@ export function genApi(options = {}) {
pouchdb.put(campaign); pouchdb.put(campaign);
}; };
const setCity = (battleId, value) => {
const campaign = u.updateIn(
get(activeCampaign),
['battles', battleId, 'city', 'selection'],
value,
);
pouchdb.put(campaign);
};
return { return {
campaigns, campaigns,
activeCampaign, activeCampaign,
event: { event: {
setCity,
setCharacter, setCharacter,
deleteCampaign, deleteCampaign,
addCampaign, addCampaign,

View File

@ -102,6 +102,33 @@ export function genChapter3Battle(battles) {
}; };
} }
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 = []) { export function genNextBattle(battles = []) {
let chapter = R.clamp(1 + parseInt(battles.length / 2), { min: 1, max: 4 }); let chapter = R.clamp(1 + parseInt(battles.length / 2), { min: 1, max: 4 });
@ -110,4 +137,6 @@ export function genNextBattle(battles = []) {
if (chapter === 2) return genChapter2Battle(battles); if (chapter === 2) return genChapter2Battle(battles);
if (chapter === 3) return genChapter3Battle(battles); if (chapter === 3) return genChapter3Battle(battles);
return genChapter4Battle(battles);
} }

View File

@ -4,6 +4,7 @@ import {
genNextBattle, genNextBattle,
genChapter2Battle, genChapter2Battle,
genChapter3Battle, genChapter3Battle,
genChapter4Battle,
} from './genNextBattle.js'; } from './genNextBattle.js';
test('generate for the first chapter', () => { test('generate for the first chapter', () => {
@ -66,3 +67,20 @@ test('chapter 3, second battle', () => {
expect(result.additionalCharacters[0].selection).toEqual('two'); expect(result.additionalCharacters[0].selection).toEqual('two');
expect(result.additionalCharacters[1].selection).toEqual('four'); 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');
});