diff --git a/README.md b/README.md index 71e6519c..660b7cba 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,11 @@ The `src/game` folder contains the actual game logic. The `src/ui` folder is the website UI where you can actually play the game. The `src/content` folder builds content for the game. +## Testing + +To test everything, run `npm run test` or `npm run test:watch`. +To test certain files, use `npm run test -- tests/actions.js` syntax. + ## How to release a new version (aka deploy) Every commit to the `main` branch automatically deploys to https://slaytheweb.cards via the Vercel service. diff --git a/astro.config.mjs b/astro.config.mjs index 78c333c2..2cb9af47 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -14,9 +14,9 @@ export default defineConfig({ preact(), AstroPWA({ registerType: 'autoUpdate', - // devOptions: { - // enabled: true, - // }, + devOptions: { + enabled: false, + }, // The actual webmanifest manifest: { name: 'Slay the Web', diff --git a/src/game/actions.js b/src/game/actions.js index 1e02dd51..b8db4ec0 100644 --- a/src/game/actions.js +++ b/src/game/actions.js @@ -1,6 +1,6 @@ import {produce, enableMapSet} from 'immer' import {clamp, shuffle} from '../utils.js' -import {isDungeonCompleted, getTargets, getCurrRoom} from './utils-state.js' +import {isDungeonCompleted, getRoomTargets, getCurrRoom} from './utils-state.js' import powers from './powers.js' import {conditionsAreValid} from './conditions.js' import {createCard, CardTargets} from './cards.js' @@ -209,10 +209,10 @@ function upgradeCard(state, {card}) { * @type {ActionFn<{card: object, target?: string}>} */ function playCard(state, {card, target}) { + if (!card) throw new Error('No card to play') if (!target) target = card.target if (typeof target !== 'string') throw new Error(`Wrong target to play card: ${target},${card.target}`) if (target === 'enemy') throw new Error('Wrong target, did you mean "enemy0" or "allEnemies"?') - if (!card) throw new Error('No card to play') if (state.player.currentEnergy < card.energy) throw new Error('Not enough energy to play card') let newState = discardCard(state, {card}) newState = produce(newState, (draft) => { @@ -273,7 +273,7 @@ export function useCardActions(state, {target, card}) { */ function addHealth(state, {target, amount}) { return produce(state, (draft) => { - const targets = getTargets(draft, target) + const targets = getRoomTargets(draft, target) targets.forEach((t) => { t.currentHealth = clamp(t.currentHealth + amount, 0, t.maxHealth) }) @@ -323,7 +323,7 @@ function addEnergyToPlayer(state, props) { */ const removeHealth = (state, {target, amount}) => { return produce(state, (draft) => { - getTargets(draft, target).forEach((t) => { + getRoomTargets(draft, target).forEach((t) => { if (t.powers.vulnerable) amount = powers.vulnerable.use(amount) let amountAfterBlock = t.block - amount if (amountAfterBlock < 0) { @@ -345,7 +345,7 @@ const removeHealth = (state, {target, amount}) => { */ const setHealth = (state, {target, amount}) => { return produce(state, (draft) => { - getTargets(draft, target).forEach((t) => { + getRoomTargets(draft, target).forEach((t) => { t.currentHealth = amount }) }) @@ -593,7 +593,7 @@ function dealDamageEqualToBlock(state, {target}) { */ function dealDamageEqualToVulnerable(state, {target}) { return produce(state, (draft) => { - getTargets(draft, target).forEach((t) => { + getRoomTargets(draft, target).forEach((t) => { if (t.powers.vulnerable) { const amount = t.currentHealth - t.powers.vulnerable t.currentHealth = amount @@ -609,7 +609,7 @@ function dealDamageEqualToVulnerable(state, {target}) { */ function dealDamageEqualToWeak(state, {target}) { return produce(state, (draft) => { - getTargets(draft, target).forEach((t) => { + getRoomTargets(draft, target).forEach((t) => { if (t.powers.weak) { const amount = t.currentHealth - t.powers.weak t.currentHealth = amount @@ -625,7 +625,7 @@ function dealDamageEqualToWeak(state, {target}) { */ function setPower(state, {target, power, amount}) { return produce(state, (draft) => { - getTargets(draft, target).forEach((target) => { + getRoomTargets(draft, target).forEach((target) => { target.powers[power] = amount }) }) diff --git a/src/game/backend.js b/src/game/backend.js index 8c5271de..62c66637 100644 --- a/src/game/backend.js +++ b/src/game/backend.js @@ -5,6 +5,7 @@ const apiUrl = 'https://api.slaytheweb.cards/api/runs' /** * @typedef {object} Run + * @prop {string} id * @prop {string} player - user inputted player name * @prop {object} gameState - the final state * @prop {PastEntry[]} gamePast - a list of past states diff --git a/src/game/utils-state.js b/src/game/utils-state.js index 6444b8c5..d4da45bf 100644 --- a/src/game/utils-state.js +++ b/src/game/utils-state.js @@ -40,12 +40,12 @@ export function getCurrRoom(state) { } /** - * Returns an array of targets (player or monsters) in the current room. + * Returns a list of "targets" (player or monsters) in the current room. * @param {State} state - * @param {CardTargets} targetQuery - like player, enemy0, enemy1 + * @param {CardTargets} targetQuery - like player, enemy0, enemy1, enemy2, allEnemies * @returns {Array} */ -export function getTargets(state, targetQuery) { +export function getRoomTargets(state, targetQuery) { if (!targetQuery || typeof targetQuery !== 'string') throw new Error('Bad query string') const room = getCurrRoom(state) diff --git a/src/ui/components/publish-run.js b/src/ui/components/publish-run.js index b9638cab..e5655c92 100644 --- a/src/ui/components/publish-run.js +++ b/src/ui/components/publish-run.js @@ -31,7 +31,7 @@ export function PublishRun({game}) { ? html`

${loading ? 'Submitting…' : ''}

diff --git a/src/ui/components/pwa.ts b/src/ui/components/pwa.ts index 89cd79cb..75e2902d 100644 --- a/src/ui/components/pwa.ts +++ b/src/ui/components/pwa.ts @@ -19,6 +19,7 @@ window.addEventListener('load', () => { pwaToast.classList.remove('show', 'refresh') } + const showPwaToast = (offline: boolean) => { if (!offline) pwaRefreshBtn.addEventListener('click', refreshCallback) requestAnimationFrame(() => { diff --git a/src/ui/components/run-stats.js b/src/ui/components/run-stats.js index 2fc71543..8a62f3b5 100644 --- a/src/ui/components/run-stats.js +++ b/src/ui/components/run-stats.js @@ -1,7 +1,9 @@ import {html, useState, useEffect} from '../lib.js' import {getRun} from '../../game/backend.js' import {getEnemiesStats} from './dungeon-stats.js' -import { SlayMap } from './slay-map.js' +import {SlayMap} from './slay-map.js' +import {Card} from './cards.js' +import {createCard} from '../../game/cards.js' export default function RunStats() { /** @type {ReturnType>} */ @@ -45,35 +47,53 @@ export default function RunStats() { console.log('extraStats', extraStats) return html` -

Slay the Web run no. ${run.id}

+
+

Slay the Web run no. ${run.id}

+
-

- ${run.player} made it to floor ${state.dungeon.y} and - ${state.won ? 'won' : 'lost'}. -

-

- The run took ${duration} on ${date}.

-

- Player made ${run.gamePast.length} moves over ${run.gameState.turn} turns,
- and ended with ${state.player.currentHealth}/${state.player.maxHealth} health. -

+
+

+ ${run.player} made it to floor ${state.dungeon.y} and + ${state.won ? 'won' : 'lost'}. +

+

The run took ${duration} on ${date}.

+

+ Player made ${run.gamePast.length} moves over ${run.gameState.turn} turns,
+ and ended with ${state.player.currentHealth}/${state.player.maxHealth} health. +

+ + ${extraStats && ( +

+ You encountered {extraStats.encountered} monsters. And killed {extraStats.killed} of them. +

+ )} +
- ${extraStats && ( +

- You encountered {extraStats.encountered} monsters. And killed {extraStats.killed} of them. + Inspect the raw JSON data for the run api.slaytheweb.cards/api/runs/${run.id}.

- )} +
+ +
+

Final deck had ${state.deck.length} cards:

+
+ ${state.deck.map((card) => + Card({ + card: createCard(card.replace('+', ''), card.includes('+') ? true : false), + }), + )} +
+
-

Final deck had ${state.deck.length} cards:

- -

- Inspect the raw data here: - api.slaytheweb.cards/api/runs/${run.id}. -

- - <${SlayMap} dungeon=${run.gameState.dungeon} x=${state.dungeon.x} y=${state.dungeon.y} scatter=${20} debug=${true}> + <${SlayMap} + dungeon=${run.gameState.dungeon} + x=${state.dungeon.x} + y=${state.dungeon.y} + scatter=${20} + debug=${true} + > ` } diff --git a/src/ui/components/slay-map-demo.js b/src/ui/components/slay-map-demo.js index 7e87b652..b569c558 100644 --- a/src/ui/components/slay-map-demo.js +++ b/src/ui/components/slay-map-demo.js @@ -14,7 +14,7 @@ const MapDemo = () => { return html`
-
+
Options <${DungeonConfig} onUpdate=${(config) => setDungeon(Dungeon(config))} />
diff --git a/src/ui/components/slay-map.js b/src/ui/components/slay-map.js index 0acb5452..2626895a 100644 --- a/src/ui/components/slay-map.js +++ b/src/ui/components/slay-map.js @@ -28,7 +28,7 @@ export class SlayMap extends Component { } componentDidUpdate(prevProps) { - const newDungeon = this.props.dungeon.id !== prevProps.dungeon.id + const newDungeon = this.props.dungeon.id !== prevProps?.dungeon.id // Let CSS know about the amount of rows and cols we have. this.base.style.setProperty('--rows', Number(this.props.dungeon.graph.length)) @@ -154,12 +154,9 @@ export class SlayMap extends Component { if (isEmpty(currentNode.edges)) { dungeon.paths = generatePaths(dungeon.graph) - console.log('genereated new dungeon paths', {dungeon}) - // debugger + if (this.debug) console.log('generated new dungeon paths', {dungeon}) } - console.log('edges from current map node', currentNode) - return html` ${dungeon.graph.map( diff --git a/src/ui/components/slay-the-web.js b/src/ui/components/slay-the-web.js index 7de25035..931cf987 100644 --- a/src/ui/components/slay-the-web.js +++ b/src/ui/components/slay-the-web.js @@ -1,9 +1,9 @@ import {html, render, Component} from '../lib.js' import SplashScreen from './splash-screen.js' -import GameScreen from './game-screen.js' import WinScreen from './win-screen.js' +import GameScreen from './game-screen.js' import '../styles/index.css' -import {init as initSounds} from '../sounds.js' +// import {init as initSounds} from '../sounds.js' /** @enum {string} */ const GameModes = { @@ -21,6 +21,7 @@ export default class SlayTheWeb extends Component { super() const urlParams = new URLSearchParams(window.location.search) const initialGameMode = urlParams.has('debug') ? GameModes.gameplay : GameModes.splash + this.state = {gameMode: initialGameMode} this.handleNewGame = this.handleNewGame.bind(this) diff --git a/src/ui/components/splash-screen.js b/src/ui/components/splash-screen.js index b73db85c..46dc5ab2 100644 --- a/src/ui/components/splash-screen.js +++ b/src/ui/components/splash-screen.js @@ -1,26 +1,7 @@ import {html, Component} from '../lib.js' -import gsap from '../animations.js' import {getRuns} from '../../game/backend.js' - -function formatDate(timestamp) { - return new Intl.DateTimeFormat('en', { - dateStyle: 'medium', - // month: 'short', - // timeStyle: 'short', - hour12: false, - }).format(new Date(timestamp)) -} - -function timeSince(timestamp) { - const seconds = Math.floor((Date.now() - timestamp) / 1000) - if (seconds < 60) return 'just now' - if (seconds < 120) return 'a minute ago' - if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago` - if (seconds < 7200) return 'an hour ago' - if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago` - if (seconds < 172800) return 'yesterday' - return `${Math.floor(seconds / 86400)} days ago` -} +import {timeSince} from '../../utils.js' +import gsap from '../animations.js' export default class SplashScreen extends Component { constructor() { @@ -31,15 +12,15 @@ export default class SplashScreen extends Component { componentDidMount() { getRuns().then(({runs}) => this.setState({runs})) - gsap.from(this.base, {duration: 0.4, autoAlpha: 0, scale: 0.98}) + gsap.from(this.base, {duration: 0.3, autoAlpha: 0, scale: 0.98}) gsap.to(this.base.querySelector('.Splash-spoder'), {delay: 5, x: 420, y: 60, duration: 3}) } render(props, state) { - const run = this.state.runs[0] + const run = state.runs[0] return html` -
` diff --git a/src/ui/components/win-screen.js b/src/ui/components/win-screen.js index 75a9173d..cac3645e 100644 --- a/src/ui/components/win-screen.js +++ b/src/ui/components/win-screen.js @@ -1,7 +1,7 @@ import {html} from '../lib.js' const WinScreen = (props) => html` -
+

Well done. You won.

diff --git a/src/ui/dragdrop.js b/src/ui/dragdrop.js index 91fbca83..e5d1e1b4 100644 --- a/src/ui/dragdrop.js +++ b/src/ui/dragdrop.js @@ -12,11 +12,12 @@ function animateCardToHand(draggable) { } /** - * - * @param {HTMLElement} target - * @param {HTMLElement} targetEl + * This gets called continously while dragging a card + * @param {HTMLElement} target - element being dragged + * @param {HTMLElement} targetEl - element below the target */ function canDropOnTarget(target, targetEl) { + if (!targetEl) return false const hasValidTarget = cardHasValidTarget( target.getAttribute('data-card-target'), getTargetStringFromElement(targetEl), diff --git a/src/ui/pages/collection.astro b/src/ui/pages/collection.astro index 54b498eb..f141f842 100644 --- a/src/ui/pages/collection.astro +++ b/src/ui/pages/collection.astro @@ -6,31 +6,43 @@ import {cards} from '../../content/cards.js' import {Card} from '../components/cards.js' import {createCard} from '../../game/cards' --- + -
-
-

Slay the Web

-

{cards.length} Card Collection

-

Hover each card to see its upgraded version.

-
+
+

← Highscores

+ +
+

Slay the Web

+

{cards.length} Card Collection

+
-
- -
+
+

Would you like to create your own card? Or tune an existing one?

+

+ This game is is open source. You can change any part of the game. If you like, there's a chat on + #slaytheweb:matrix.org + and I'll help you get started. +

+

+
-
-
- { - cards.map((card) => ( -
- - -
- )) - } -
-
-
+
+
+ { + cards.map((card) => ( +
+ + +
+ )) + } +
+
+
diff --git a/src/ui/pages/map-demo.astro b/src/ui/pages/map-demo.astro index aa265a2a..89a9816c 100644 --- a/src/ui/pages/map-demo.astro +++ b/src/ui/pages/map-demo.astro @@ -23,6 +23,12 @@ import '../styles/index.css'