Skip to content

Commit

Permalink
Merge pull request #244 from oskarrough/refactor/run-stats-page
Browse files Browse the repository at this point in the history
Refactor/run stats page
  • Loading branch information
oskarrough authored Dec 16, 2024
2 parents 0b4b617 + 3ccfc02 commit 6de87a2
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 180 deletions.
5 changes: 2 additions & 3 deletions src/game/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,11 @@ export async function postRun(game, playerName) {
}

/**
* @returns {Promise<Run[]>} list of game runs
* @returns {Promise<{runs: Run[], total: number}>} list of game runs
*/
export async function getRuns() {
const res = await fetch(apiUrl)
const {runs} = await res.json()
return runs
return await res.json()
}

/**
Expand Down
11 changes: 11 additions & 0 deletions src/game/utils-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ export function cardHasValidTarget(cardTarget, targetQuery) {
)
}

/**
* Can't even begin to explain this one. Needs refactor.
* @param {HTMLElement} el
* @returns {string}
*/
export function getTargetStringFromElement(el) {
const targetIndex = Array.from(el.parentNode.children).indexOf(el)
return el.dataset.type + targetIndex
}


/**
* @param {Room} room
* @returns {boolean} true if the room has been cleared.
Expand Down
66 changes: 66 additions & 0 deletions src/ui/components/run-stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {getRun} from '../../game/backend.js'
import {getEnemiesStats} from './dungeon-stats.js'
import {html, useState, useEffect} from '../lib.js'

export default function RunStats() {
const [run, setRun] = useState()
const [id, setId] = useState()

useEffect(() => {
const params = new URLSearchParams(window.location.search)
setId(params.get('id'))
}, [])

useEffect(() => {
getRun(id).then((what) => {
setRun(what)
console.log(what)
})
}, [id])

if (!run) return html`<h1>Loading statistics for run no. ${id}...</h1>`

const state = run.gameState
const date = new Intl.DateTimeFormat('en', {
dateStyle: 'long',
hour12: false,
}).format(new Date(state.createdAt))

const ms = state.endedAt - state.createdAt
const hours = Math.floor(ms / (1000 * 60 * 60))
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((ms / 1000) % 60)
const duration = `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds}s`

// Not all runs have this data in the backend.
let extraStats = state.dungeon.graph ? getEnemiesStats(state.dungeon) : null
console.log('extraStats', extraStats)

return html`
<h1>Slay the Web run no. ${run.id}</h1>
<div className="Box Box--text Box--full">
<p>
<em>${run.player}</em> made it to floor ${state.dungeon.y} and
<strong> ${state.won ? 'won' : 'lost'}</strong> in ${duration} on ${date} with
${state.player.currentHealth}/${state.player.maxHealth} health.
</p>
</div>
${extraStats && (
<p>
You encountered {extraStats.encountered} monsters. And killed {extraStats.killed} of them.
</p>
)}
<p>Final deck had ${state.deck.length} cards:</p>
<ul>
${state.deck.map((card) => <li>{card}</li>)}
</ul>
<p>
Feel free to inspect the data yourself:
<a href=${'https://api.slaytheweb.cards/api/runs/' + run.id}>api.slaytheweb.cards/api/runs/${run.id}</a
>.
</p>
`
}
65 changes: 32 additions & 33 deletions src/ui/dragdrop.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Draggable} from 'gsap/Draggable.js'
import {cardHasValidTarget} from '../game/utils-state.js'
import { Draggable } from 'gsap/Draggable.js'
import { cardHasValidTarget, getTargetStringFromElement } from '../game/utils-state.js'
import gsap from './animations.js'
import sounds from './sounds.js'

Expand All @@ -8,16 +8,21 @@ const overClass = 'is-dragOver'

/** Makes the card fly back into the hand */
function animateCardToHand(draggable) {
return gsap.to(draggable.target, {x: draggable.startX, y: draggable.startY, zIndex: 0})
return gsap.to(draggable.target, { x: draggable.startX, y: draggable.startY, zIndex: 0 })
}

/**
* @param {HTMLElement} el
* @returns {string}
*
* @param {HTMLElement} target
* @param {HTMLElement} targetEl
*/
function getTargetStringFromElement(el) {
const targetIndex = Array.from(el.parentNode.children).indexOf(el)
return el.dataset.type + targetIndex
function canDropOnTarget(target, targetEl) {
const hasValidTarget = cardHasValidTarget(
target.getAttribute('data-card-target'),
getTargetStringFromElement(targetEl),
)
const targetIsDead = targetEl.classList.contains('Target--isDead')
return hasValidTarget && !targetIsDead
}

/**
Expand All @@ -26,54 +31,48 @@ function getTargetStringFromElement(el) {
* @param {Function} afterRelease
*/
export default function enableDragDrop(container, afterRelease) {
/** @type {NodeListOf<HTMLElement>} */
const targets = container.querySelectorAll('.Target')
const cards = container.querySelectorAll('.Hand .Card')

cards.forEach((card) => {
Draggable.create(card, {

onDragStart() {
sounds.selectCard()
},
// While dragging, highlight any targets we are dragging over.

onDrag() {
if (this.target.attributes.disabled) {
const cardEl = this.target

if (cardEl.attributes.disabled) {
this.endDrag()
}
let i = targets.length
while (--i > -1) {
// Highlight only if valid target.
if (this.hitTest(targets[i], '40%')) {
if (
cardHasValidTarget(
this.target.getAttribute('data-card-target'),
getTargetStringFromElement(targets[i]),
)
) {
targets[i].classList.add(overClass)
}

for (const targetEl of targets) {
if (this.hitTest(targetEl, '40%') && canDropOnTarget(cardEl, targetEl)) {
targetEl.classList.add(overClass)
} else {
targets[i].classList.remove(overClass)
targetEl.classList.remove(overClass)
}
}
},

onRelease() {
const cardEl = this.target

// Which element are we dropping on?
// Find the (first) DOM element we dropped the card on.
let targetEl
let i = targets.length
while (--i > -1) {
if (this.hitTest(targets[i], '40%')) {
targetEl = targets[i]
for (const t of targets) {
if (this.hitTest(t, '40%')) {
targetEl = t
break
}
}

if (!targetEl) return animateCardToHand(this)

// If card is allowed here, trigger the callback with target, else animate back.
const targetString = getTargetStringFromElement(targetEl)
if (cardHasValidTarget(cardEl.getAttribute('data-card-target'), targetString)) {
// Either trigger the callback with valid target, or animate the card back in back.
if (canDropOnTarget(cardEl, targetEl)) {
const targetString = getTargetStringFromElement(targetEl)
afterRelease(cardEl.dataset.id, targetString, cardEl)
} else {
animateCardToHand(this)
Expand Down
156 changes: 79 additions & 77 deletions src/ui/pages/stats.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,91 +3,93 @@ import Layout from '../layouts/Layout.astro'
import {getRuns} from '../../game/backend.js'
import '../styles/typography.css'
const runs = (await getRuns()).reverse()
let {runs, total} = await getRuns()
---

<Layout title="Statistics & Highscores">
<article class="Container">
<div class="Box">
<ul class="Options">
<li><a class="Button" href="/">Back</a></li>
</ul>
</div>
<article class="Container">
<div class="Box">
<ul class="Options">
<li><a class="Button" href="/">Back</a></li>
</ul>
</div>

<h1>Statistics & Highscores for Slay the Web</h1>
<h1>Statistics & Highscores for Slay the Web</h1>

<div class="Box Box--text Box--full">
<p>
A chronological list of Slay the Web runs.<br />
There is quite a bit of statistics that could be gathered from the runs, and isn't yet shown here. <a
href="https://matrix.to/#/#slaytheweb:matrix.org"
rel="nofollow">Chat on #slaytheweb:matrix.org</a>
</p>
</div>
<table>
<thead>
<tr>
<th>Player</th>
<th>Win?</th>
<th>Floor</th>
<th align="right">Time</th>
<th align="right">Date</th>
</tr>
</thead>
<tbody>
{
runs?.length
? runs.map((run) => {
const state = run.gameState
const date = new Intl.DateTimeFormat('en', {
dateStyle: 'long',
// timeStyle: 'short',
hour12: false,
}).format(new Date(run.createdAt))
<div class="Box Box--text Box--full">
<p>
A chronological list of <strong>{total}</strong> Slay the Web runs. Although we only show 200 latest here
because I did not implement pagination and else the server timeouts..<br />
There is quite a bit of statistics that could be gathered from the runs, and isn't yet shown here. <a
href="https://matrix.to/#/#slaytheweb:matrix.org"
rel="nofollow">Chat on #slaytheweb:matrix.org</a
>
</p>
</div>
<table>
<thead>
<tr>
<th>Player</th>
<th>Win?</th>
<th>Floor</th>
<th align="right">Time</th>
<th align="right">Date</th>
</tr>
</thead>
<tbody>
{
runs?.length
? runs.map((run) => {
const state = run.gameState
const date = new Intl.DateTimeFormat('en', {
dateStyle: 'long',
// timeStyle: 'short',
hour12: false,
}).format(new Date(run.createdAt))

let duration = 0
if (run.endedAt) {
const ms = run.endedAt - run.createdAt
const hours = Math.floor(ms / (1000 * 60 * 60))
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((ms / 1000) % 60)
duration = `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds}s`
}
let duration = 0
if (run.endedAt) {
const ms = run.endedAt - run.createdAt
const hours = Math.floor(ms / (1000 * 60 * 60))
const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((ms / 1000) % 60)
duration = `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds}s`
}

return (
<tr>
<td>
<a href={`/stats/` + run.id}>
{run.id}. {run.player}
</a>
</td>
<td>{run.won ? 'WIN' : 'LOSS'}</td>
<td>{run.floor}</td>
<td align="right">{duration}</td>
<td align="right">{date}</td>
</tr>
)
})
: 'Loading...'
}
</tbody>
</table>
<p>
If you want your run removed, <a href="https://matrix.to/#/#slaytheweb:matrix.org">let me know</a>.
</p>
</article>
return (
<tr>
<td>
<a href={`/stats/run?id=` + run.id}>
{run.id}. {run.player}
</a>
</td>
<td>{run.won ? 'WIN' : 'LOSS'}</td>
<td>{run.floor}</td>
<td align="right">{duration}</td>
<td align="right">{date}</td>
</tr>
)
})
: 'Loading...'
}
</tbody>
</table>
<p>
If you want your run removed, <a href="https://matrix.to/#/#slaytheweb:matrix.org">let me know</a>.
</p>
</article>
</Layout>

<style>
table {
width: 100%;
border-spacing: 0;
}
tbody tr:nth-child(odd) {
background-color: #eee;
}
th,
td {
text-align: left;
}
table {
width: 100%;
border-spacing: 0;
}
tbody tr:nth-child(odd) {
background-color: #eee;
}
th,
td {
text-align: left;
}
</style>
Loading

0 comments on commit 6de87a2

Please sign in to comment.