From 7a5e85b8f7cc3f2ef7879bf773f89ee80f69d01c Mon Sep 17 00:00:00 2001 From: xangelo Date: Tue, 21 Jan 2025 23:19:22 -0500 Subject: [PATCH] feat: make travel an idle activity instead of clicking "next step" for each step during traveling, the game will automatically move you to the next step every second (this is adjustable). --- migrations/20250120182103_travel_timer.ts | 19 ++++ public/assets/css/game.css | 8 ++ public/assets/css/travel.css | 3 + src/server/api.ts | 48 +++++----- src/server/map.ts | 8 +- src/server/routes/travel.ts | 107 ++++++++-------------- src/server/views/travel.ts | 85 ++++++++++------- src/shared/map.ts | 2 - src/shared/travel.ts | 4 +- 9 files changed, 148 insertions(+), 136 deletions(-) create mode 100644 migrations/20250120182103_travel_timer.ts create mode 100644 public/assets/css/travel.css diff --git a/migrations/20250120182103_travel_timer.ts b/migrations/20250120182103_travel_timer.ts new file mode 100644 index 0000000..89ea274 --- /dev/null +++ b/migrations/20250120182103_travel_timer.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +export async function up(knex: Knex): Promise { + return knex.schema.alterTable('travel', table => { + table.timestamp('start_time').notNullable().defaultTo(knex.fn.now()); + table.timestamp('end_time').notNullable().defaultTo(knex.fn.now()); + table.dropColumn('total_distance'); + table.dropColumn('current_position'); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.alterTable('travel', table => { + table.dropColumn('start_time'); + table.dropColumn('end_time'); + table.integer('total_distance').notNullable().defaultTo(0); + table.integer('current_position').notNullable().defaultTo(0); + }); +} diff --git a/public/assets/css/game.css b/public/assets/css/game.css index 75d9a2d..37c3af7 100644 --- a/public/assets/css/game.css +++ b/public/assets/css/game.css @@ -9,6 +9,7 @@ @import 'explore.css'; @import 'store.css'; @import 'combat.css'; +@import 'travel.css'; .equipment-slot { position: relative; @@ -115,6 +116,13 @@ mix-blend-mode: color-burn; } +.time-remaining { + text-align: center; + font-size: 1.2rem; + margin: 0.5rem 0; + color: #d9975a; +} + #view { font-size: 14px; padding: 1rem; diff --git a/public/assets/css/travel.css b/public/assets/css/travel.css new file mode 100644 index 0000000..44e80cb --- /dev/null +++ b/public/assets/css/travel.css @@ -0,0 +1,3 @@ +#travel-plan { + height: 20px; +} \ No newline at end of file diff --git a/src/server/api.ts b/src/server/api.ts index 76a722a..cac9360 100644 --- a/src/server/api.ts +++ b/src/server/api.ts @@ -17,7 +17,7 @@ import {broadcastMessage} from '@shared/message'; import { Player } from '@shared/player'; import {createFight, getMonsterList, getMonsterLocation, getRandomMonster, loadMonster, loadMonsterFromFight} from './monster'; import {FightTrigger, Monster} from '@shared/monsters'; -import { getAllPaths, getAllServices, getCityDetails, getService, getTravelPlan, getDungeon } from './map'; +import { getAllPaths, getAllServices, getCityDetails, getService, getTravelPlan, getDungeon, clearTravelPlan, completeTravel } from './map'; import { signup, login, authEndpoint } from './auth'; import {db} from './lib/db'; import { getPlayerSkills} from './skills'; @@ -34,7 +34,7 @@ import { renderMap } from './views/map'; import { renderSkills } from './views/skills'; import { renderMonsterSelector, renderOnlyMonsterSelector } from './views/monster-selector'; import { renderFight, renderFightPreRound, renderRoundDetails } from './views/fight'; -import { renderTravel, travelButton } from './views/travel'; +import { renderTravel } from './views/travel'; import { renderAdminActions } from './views/admin'; // TEMP! @@ -176,10 +176,7 @@ app.get('/player/explore', authEndpoint, async (req: Request, res: Response) => const fight = await loadMonsterFromFight(req.player.id); const travelPlan = await getTravelPlan(req.player.id); let closestTown = req.player.city_id; - - if(travelPlan) { - closestTown = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id; - } + const now = Date.now(); if(fight && req.player.hp > 0) { const location = await getMonsterLocation(fight.ref_id); @@ -198,26 +195,25 @@ app.get('/player/explore', authEndpoint, async (req: Request, res: Response) => return; } - if(travelPlan && req.player.hp > 0) { - // traveling! - const chanceToSeeMonster = random(0, 100); - const things: any[] = []; - if(chanceToSeeMonster <= 30) { - const monster = await getRandomMonster([closestTown]); - things.push(monster); + if(travelPlan) { + const startTime = Date.parse(travelPlan.start_time); + const endTime = Date.parse(travelPlan.end_time); + // the player has completed their travel + if(endTime < now) { + logger.info(`Completing travel for player [${req.player.id}]`); + await completeTravel(req.player.id); + } + else { + // check if your current time is closer to end_time or start_time + closestTown = (now - startTime) < (endTime - now) ? travelPlan.source_id : travelPlan.destination_id; + + res.send(renderPlayerBar(req.player) + renderTravel({ + closestTown: closestTown, + walkingText: '', + travelPlan + })); + return; } - - // STEP_DELAY - const nextAction = cache[`step:${req.player.id}`] || 0; - - res.send(renderPlayerBar(req.player) + renderTravel({ - things, - nextAction, - closestTown: closestTown, - walkingText: '', - travelPlan - })); - return; } // display the default explore view @@ -313,8 +309,6 @@ app.post('/fight/turn', authEndpoint, async (req: Request, res: Response) => { if(monster.fight_trigger === 'travel' && fightData.roundData.winner === 'player') { // you're travellinga dn you won.. display the keep walking! const travelPlan = await getTravelPlan(req.player.id); - const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id; - travelSection = travelButton(0); } const playerBar = renderPlayerBar(fightData.player); diff --git a/src/server/map.ts b/src/server/map.ts index 45975bd..4960077 100644 --- a/src/server/map.ts +++ b/src/server/map.ts @@ -66,13 +66,13 @@ export async function travel(player: Player, dest_id: number): Promise { throw new Error('Unexpected response when moving'); } + await db('players').where({id: player_id}).update({ + city_id: rows[0].destination_id + }); + return rows[0] as Travel; } diff --git a/src/server/routes/travel.ts b/src/server/routes/travel.ts index ba2f70c..d400659 100644 --- a/src/server/routes/travel.ts +++ b/src/server/routes/travel.ts @@ -6,81 +6,14 @@ import { getRandomMonster, loadMonsterFromFight, getMonsterLocation } from '../m import { clearTravelPlan, completeTravel, getAllPaths, getAllServices, getCityDetails, getService, getTravelPlan, stepForward, travel, getDungeon } from '../map'; import * as CONSTANT from '../../shared/constants'; import * as Alert from '../views/alert'; -import { renderTravel, travelButton } from '../views/travel'; +import { renderTravel, renderTravelComplete, renderTravelProgress } from '../views/travel'; import { renderMap } from '../views/map'; import { renderPlayerBar } from '../views/player-bar' import { renderFightPreRound } from '../views/fight'; +import { logger } from '@server/lib/logger'; export const travelRouter = Router(); -// take a step along your travel plan -travelRouter.post('/travel/step', authEndpoint, async (req: Request, res: Response) => { - const stepTimerKey = `step:${req.player.id}`; - - const travelPlan = await getTravelPlan(req.player.id); - if(!travelPlan) { - res.send(Alert.ErrorAlert('You don\'t have a travel plan')); - return; - } - - if(req.rl.cache[stepTimerKey]) { - if(req.rl.cache[stepTimerKey] > Date.now()) { - res.send(Alert.ErrorAlert('Hmm.. travelling too quickly')); - return; - } - } - - travelPlan.current_position++; - - if(travelPlan.current_position >= travelPlan.total_distance) { - const travel = await completeTravel(req.player.id); - - req.player.city_id = travel.destination_id; - await movePlayer(travel.destination_id, req.player.id); - - const [city, locations, paths] = await Promise.all([ - getCityDetails(travel.destination_id), - getAllServices(travel.destination_id, req.player.level), - getAllPaths(travel.destination_id) - ]); - - delete req.rl.cache[stepTimerKey]; - res.send(await renderMap({city, locations, paths}, req.player.city_id)); - } - else { - const walkingText: string[] = [ - 'You take a step forward', - 'You keep moving' - ]; - // update existing plan.. - // decide if they will run into anything - const travelPlan = await stepForward(req.player.id); - - const closest: number = (travelPlan.current_position / travelPlan.total_distance) > 0.5 ? travelPlan.destination_id : travelPlan.source_id; - - const chanceToSeeMonster = random(0, 100); - const things: any[] = []; - if(chanceToSeeMonster <= 30) { - const monster = await getRandomMonster([closest]); - things.push(monster); - } - - // STEP_DELAY - const nextAction = Date.now() + CONSTANT.STEP_DELAY; - - req.rl.cache[stepTimerKey] = nextAction; - - res.send(renderTravel({ - things, - nextAction, - closestTown: closest, - walkingText: sample(walkingText), - travelPlan - })); - - } -}); - travelRouter.post('/travel/return-to-source', authEndpoint, async (req: Request, res: Response) => { // puts the player back in their starting town // doesn't matter if they don't have one @@ -125,8 +58,6 @@ travelRouter.post('/travel/:destination_id', authEndpoint, async (req: Request, const travelPlan = await travel(req.player, destination.id); res.send(renderTravel({ - things: [], - nextAction: 0, walkingText: '', closestTown: req.player.city_id, travelPlan @@ -134,3 +65,37 @@ travelRouter.post('/travel/:destination_id', authEndpoint, async (req: Request, }); +// return the curret travel plan information +travelRouter.get('/travel', authEndpoint, async (req: Request, res: Response) => { + const travelPlan = await getTravelPlan(req.player.id); + const now = Date.now(); + + if(!travelPlan) { + res.sendStatus(400); + return; + } + + logger.debug(`Travel plan: ${JSON.stringify(travelPlan)}`); + logger.debug({ + now, + start_time: travelPlan.start_time, + end_time: Date.parse(travelPlan.end_time as any) <= Date.now() ? 'complete' : travelPlan.end_time, + }); + + + if (Date.parse(travelPlan.end_time as any) <= Date.now()) { + await completeTravel(req.player.id); + res.send(renderTravelComplete({ + walkingText: '', + closestTown: travelPlan.destination_id, + travelPlan + })); + } + else { + res.send(renderTravel({ + walkingText: '', + closestTown: req.player.city_id, + travelPlan + })); + } +}); \ No newline at end of file diff --git a/src/server/views/travel.ts b/src/server/views/travel.ts index 9fcbb47..3178824 100644 --- a/src/server/views/travel.ts +++ b/src/server/views/travel.ts @@ -1,49 +1,70 @@ import { TravelDTO } from "@shared/map"; import { ProgressBar } from './components/progress-bar'; -import { ButtonWithBlock } from './components/button'; +import { BackToTown, ButtonWithBlock } from './components/button'; +import { Title } from "./components/city"; -export function travelButton(blockTime: number): string { - return ButtonWithBlock({ - id: 'keep-walking', - 'hx-post': '/travel/step' - }, 'Keep Walking', blockTime); +function formatTimeRemaining(remainingMs: number): string { + const minutes = Math.floor(remainingMs / (1000 * 60)); + const seconds = Math.floor((remainingMs % (1000 * 60)) / 1000); + + if (minutes > 0) { + return `${minutes}m ${seconds}s`; + } + return `${seconds}s`; } -export function renderTravel(data: TravelDTO): string { +export function renderTravelProgress(data: TravelDTO): string { + const now = Date.now(); + const totalDuration = Date.parse(data.travelPlan.end_time) - Date.parse(data.travelPlan.start_time); + const elapsed = now - Date.parse(data.travelPlan.start_time); + const remaining = Date.parse(data.travelPlan.end_time) - now; + const progress = Math.min(100, Math.max(0, (elapsed / totalDuration) * 100)); + + let html = ` +
+

Travelling from ${data.travelPlan.source_city_name} to ${data.travelPlan.destination_city_name}

+

Time remaining: ${formatTimeRemaining(remaining)}

+ ${ProgressBar(progress, 100, 'travel-plan', { + startingColor: '#d9975a', + endingColor: '#bb724c', + noContent: true + })} +
+ `; + return html; +} +export function renderTravel(data: TravelDTO): string { let promptText = data.walkingText; - const blockTime = data.nextAction || 0; let html = `
- -
-
-

Travelling from ${data.travelPlan.source_city_name} to ${data.travelPlan.destination_city_name}

- ${ProgressBar(data.travelPlan.current_position, data.travelPlan.total_distance, 'travel-plan', { - startingColor: '#d9975a', - endingColor: '#bb724c' - })} -
-`; +
+ ${renderTravelProgress(data)} + `; + html += '
'; - html += travelButton(blockTime); - if(data.things.length) { - // ok you found something, for now we only support - // monsters, but eventually that will change - promptText = `You see a ${data.things[0].name}`; - html += `
- - -
`; - } - - // end #travelling-actions html += '
'; - html += `

${promptText}

`; + + if(promptText) { + html += `
${promptText}
`; + } html += `

Return to ${data.travelPlan.source_city_name}

`; html += '
'; - return html; } + +export function renderTravelComplete(data: TravelDTO): string { + return ` +
+ ${Title(data.travelPlan.destination_city_name)} +
+
+

You have arrived at ${data.travelPlan.destination_city_name}!

+
+

${BackToTown()}

+
+
+ `; +} \ No newline at end of file diff --git a/src/shared/map.ts b/src/shared/map.ts index acf1f45..ac942c5 100644 --- a/src/shared/map.ts +++ b/src/shared/map.ts @@ -30,8 +30,6 @@ export type Path = { } export type TravelDTO = { - things: any[], - nextAction: number, walkingText: string, closestTown: number; travelPlan: TravelWithNames diff --git a/src/shared/travel.ts b/src/shared/travel.ts index babafa0..e5a37eb 100644 --- a/src/shared/travel.ts +++ b/src/shared/travel.ts @@ -2,8 +2,8 @@ export type Travel = { player_id: string; source_id: number; destination_id: number; - total_distance: number; - current_position: number; + start_time: string; + end_time: string; } export type TravelWithNames = Travel & { -- 2.25.1