--- /dev/null
+import { Knex } from "knex";
+
+export async function up(knex: Knex): Promise<void> {
+ 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<void> {
+ 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);
+ });
+}
@import 'explore.css';
@import 'store.css';
@import 'combat.css';
+@import 'travel.css';
.equipment-slot {
position: relative;
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;
--- /dev/null
+#travel-plan {
+ height: 20px;
+}
\ No newline at end of file
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';
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!
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);
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
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);
}
const deviation = Math.floor(path.distance * (random(3,10)/100));
- const steps = random(path.distance - deviation, path.distance + deviation);
+ const timeInSeconds = random(path.distance - deviation, path.distance + deviation);
const rows = await db('travel').insert({
player_id: player.id,
source_id: player.city_id,
destination_id: dest_id,
- total_distance: steps
+ end_time: db.raw(`now() + interval '${timeInSeconds} seconds'`)
});
return getTravelPlan(player.id);
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;
}
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
const travelPlan = await travel(req.player, destination.id);
res.send(renderTravel({
- things: [],
- nextAction: 0,
walkingText: '',
closestTown: req.player.city_id,
travelPlan
});
+// 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
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 = `
+ <div class="travel-distance">
+ <p>Travelling from ${data.travelPlan.source_city_name} to ${data.travelPlan.destination_city_name}</p>
+ <p class="time-remaining">Time remaining: ${formatTimeRemaining(remaining)}</p>
+ ${ProgressBar(progress, 100, 'travel-plan', {
+ startingColor: '#d9975a',
+ endingColor: '#bb724c',
+ noContent: true
+ })}
+ </div>
+ `;
+ return html;
+}
+export function renderTravel(data: TravelDTO): string {
let promptText = data.walkingText;
- const blockTime = data.nextAction || 0;
let html = `<section id="explore" class="tab active" hx-swap-oob="true" style="background-image: url('/assets/img/map/${data.closestTown}.jpeg')">
-
-<div id="travelling" class="city-details">
- <div class="travel-distance">
- <p>Travelling from ${data.travelPlan.source_city_name} to ${data.travelPlan.destination_city_name}</p>
- ${ProgressBar(data.travelPlan.current_position, data.travelPlan.total_distance, 'travel-plan', {
- startingColor: '#d9975a',
- endingColor: '#bb724c'
- })}
- </div>
-`;
+ <div id="travelling" class="city-details" hx-get="/travel" hx-trigger="every 1s">
+ ${renderTravelProgress(data)}
+ `;
+
html += '<div id="travelling-actions">';
- 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 += `<form hx-post="/fight" hx-target="#explore">
-<input type="hidden" name="monsterId" value="${data.things[0].id}">
-<button type="submit" class="red">Fight</button>
-</form>`;
- }
-
- // end #travelling-actions
html += '</div>';
- html += `<p>${promptText}</p>`;
+
+ if(promptText) {
+ html += `<div class="prompt">${promptText}</div>`;
+ }
html += `<p align="right"><a href="#" hx-post="/travel/return-to-source">Return to ${data.travelPlan.source_city_name}</a></p>`;
html += '</div></section>';
-
return html;
}
+
+export function renderTravelComplete(data: TravelDTO): string {
+ return `
+ <section id="explore" class="tab active" hx-swap-oob="true" style="background-image: url('/assets/img/map/${data.closestTown}.jpeg')">
+ ${Title(data.travelPlan.destination_city_name)}
+ <div id="travelling" class="city-details">
+ <div class="travel-complete">
+ <p>You have arrived at ${data.travelPlan.destination_city_name}!</p>
+ </div>
+ <p>${BackToTown()}</p>
+ </div>
+ </section>
+ `;
+}
\ No newline at end of file
}
export type TravelDTO = {
- things: any[],
- nextAction: number,
walkingText: string,
closestTown: number;
travelPlan: TravelWithNames
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 & {