Skip to content

Commit

Permalink
Merge pull request #245 from oskarrough/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
oskarrough authored Dec 17, 2024
2 parents 4f3509d + c78cfba commit 46f89d5
Show file tree
Hide file tree
Showing 22 changed files with 256 additions and 196 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
16 changes: 8 additions & 8 deletions src/game/actions.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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) {
Expand All @@ -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
})
})
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
})
})
Expand Down
1 change: 1 addition & 0 deletions src/game/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/game/utils-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<MONSTER>}
*/
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)

Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/publish-run.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function PublishRun({game}) {
? html`
<label
>Want to post your run to the public, Slay the Web highscores?<br />
<input type="text" name="playername" required placeholder="Know thyself" />
<input type="text" name="playername" maxlength="140" required placeholder="Know thyself" />
</label>
<button disabled=${loading} type="submit">Submit my run</button>
<p>${loading ? 'Submitting…' : ''}</p>
Expand Down
1 change: 1 addition & 0 deletions src/ui/components/pwa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ window.addEventListener('load', () => {

pwaToast.classList.remove('show', 'refresh')
}

const showPwaToast = (offline: boolean) => {
if (!offline) pwaRefreshBtn.addEventListener('click', refreshCallback)
requestAnimationFrame(() => {
Expand Down
72 changes: 46 additions & 26 deletions src/ui/components/run-stats.js
Original file line number Diff line number Diff line change
@@ -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<typeof useState<import('../../game/backend.js').Run>>} */
Expand Down Expand Up @@ -45,35 +47,53 @@ export default function RunStats() {
console.log('extraStats', extraStats)

return html`
<h1>Slay the Web run no. ${run.id}</h1>
<header class="Header">
<h1>Slay the Web run no. ${run.id}</h1>
</header>
<p>
<em>${run.player}</em> made it to floor ${state.dungeon.y} and
<strong> ${state.won ? 'won' : 'lost'}</strong>.
</p>
<p>
The run took ${duration} on ${date}.</p>
<p>
Player made ${run.gamePast.length} moves over ${run.gameState.turn} turns,<br/>
and ended with ${state.player.currentHealth}/${state.player.maxHealth} health.
</p>
<div class="Box Box--text">
<p>
<em>${run.player}</em> made it to floor ${state.dungeon.y} and
<strong> ${state.won ? 'won' : 'lost'}</strong>.
</p>
<p>The run took ${duration} on ${date}.</p>
<p>
Player made ${run.gamePast.length} moves over ${run.gameState.turn} turns,<br />
and ended with ${state.player.currentHealth}/${state.player.maxHealth} health.
</p>
${extraStats && (
<p>
You encountered {extraStats.encountered} monsters. And killed {extraStats.killed} of them.
</p>
)}
</div>
${extraStats && (
<div class="Box">
<p>
You encountered {extraStats.encountered} monsters. And killed {extraStats.killed} of them.
Inspect the raw JSON data for the run <a href=${'https://api.slaytheweb.cards/api/runs/' + run.id}
>api.slaytheweb.cards/api/runs/${run.id}</a
>.
</p>
)}
</div>
<div class="Box">
<p>Final deck had ${state.deck.length} cards:</p>
<div class="Cards Cards--grid Cards--mini">
${state.deck.map((card) =>
Card({
card: createCard(card.replace('+', ''), card.includes('+') ? true : false),
}),
)}
</div>
</div>
<p>Final deck had ${state.deck.length} cards:</p>
<ul>
${state.deck.map((card) => <li>{card}</li>)}
</ul>
<p>
Inspect the raw data here:
<a href=${'https://api.slaytheweb.cards/api/runs/' + run.id}>api.slaytheweb.cards/api/runs/${run.id}</a
>.
</p>
<${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}
><//>
`
}
2 changes: 1 addition & 1 deletion src/ui/components/slay-map-demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const MapDemo = () => {

return html`
<div class="Box">
<details>
<details open>
<summary>Options</summary>
<${DungeonConfig} onUpdate=${(config) => setDungeon(Dungeon(config))} />
</details>
Expand Down
7 changes: 2 additions & 5 deletions src/ui/components/slay-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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`
<slay-map>
${dungeon.graph.map(
Expand Down
5 changes: 3 additions & 2 deletions src/ui/components/slay-the-web.js
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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)
Expand Down
42 changes: 11 additions & 31 deletions src/ui/components/splash-screen.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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`
<article class="Container Splash--fadein">
<article class="Splash Container">
<header class="Header">
<h1>Slay the Web</h1>
<h2>A card crawl adventure for you and your browser</h2>
Expand All @@ -59,15 +40,14 @@ export default class SplashScreen extends Component {
<li><a class="Button" href="/collection">Collection</a></li>
<li>
<a class="Button" href="/stats">Highscores</a>
${this.state.runs.length > 0 ? html`
<a class="LastRun" href=${`/stats/run?id=${this.state.runs[0].id}`}>
${timeSince(this.state.runs[0].createdAt)} someone ${this.state.runs[0].won ? 'won' : 'lost'}
</a>` : ''}
${this.state.runs.length > 0
? html` <a class="LastRun" href=${`/stats/run?id=${run.id}`}>
${timeSince(run.createdAt)} someone ${run.won ? 'won' : 'lost'}
</a>`
: ''}
</li>
</ul>
<p center>
<a href="/changelog">Changelog</a> & <a href="/manual">Manual</a>
</p>
<p center><a href="/changelog">Changelog</a> & <a href="/manual">Manual</a></p>
</div>
</article>
`
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/win-screen.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {html} from '../lib.js'

const WinScreen = (props) => html`
<article class="Splash">
<article>
<h1>Well done. You won.</h1>
<p><button autofocus onClick=${props.onNewGame}>Play again</a></p>
</article>
Expand Down
7 changes: 4 additions & 3 deletions src/ui/dragdrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading

0 comments on commit 46f89d5

Please sign in to comment.