diff --git a/src/modules/game/game.ts b/src/modules/game/game.ts
index 371dd8f..30202b6 100644
--- a/src/modules/game/game.ts
+++ b/src/modules/game/game.ts
@@ -1,8 +1,19 @@
import { MatrixClient } from "matrix-js-sdk";
import type { ICallbackStore } from "../types.js";
import { config } from "../../config.js";
-import { getPlayerById, getRace } from "../../services/game/entity.js";
import {
+ existsEntity,
+ getEntitiesAtLocation,
+ getEntityByName,
+ getHealthPercentage,
+ getMaxHealth,
+ getNpcsAtLocation,
+ getPlayer,
+ getPlayersAtLocation,
+ getRace,
+} from "../../services/game/entity.js";
+import {
+ entitiesShareLocation,
existsLocation,
getLocation,
getLocationByName,
@@ -16,6 +27,7 @@ import { getLevel } from "../../services/game/entity.js";
import {
getFullItemName,
getTotalStats,
+ type IEntity,
type IItem,
type IStat,
type Location,
@@ -51,9 +63,17 @@ const registerModuleGame = (
callbackFunc: onNearby,
});
callbackStore.messageCallbacks.push({
- startConditions: [`${gamePrefix} travel`],
+ startConditions: [`${gamePrefix} travel `],
callbackFunc: onTravel,
});
+ callbackStore.messageCallbacks.push({
+ startConditions: [`${gamePrefix} entities`],
+ callbackFunc: onEntities,
+ });
+ callbackStore.messageCallbacks.push({
+ startConditions: [`${gamePrefix} entity `],
+ callbackFunc: onEntity,
+ });
};
const onHelp = (_text: string, roomId: string) => {
@@ -68,6 +88,8 @@ const onHelp = (_text: string, roomId: string) => {
!game location - Shows information about your current location
!game nearby - Shows nearby locations
!game travel {location} - Travel to a location
+ !game entities - Shows entities at your location
+ !game entity {entity} - Shows information about an entity
(WIP) !game talk {entity} - Talk to an entity
(WIP) !game fight {entity} - Fight an entity
(WIP) !game work {action} - Start work
@@ -76,37 +98,38 @@ const onHelp = (_text: string, roomId: string) => {
};
const onStatus = (_text: string, roomId: string, sender: string) => {
- const player = getPlayerById(sender);
- const race = getRace(player.race);
- const location = getLocation(player.location);
+ const entity = getEntityByName(sender);
+ const race = getRace(entity.race);
+ const location = getLocation(entity.location);
- const level = getLevel(player.experience);
+ const level = getLevel(entity.experience);
client.sendHtmlMessage(
roomId,
"",
`
- - Name: ${player.name}
- - Description: ${player.description}
+ - Name: ${entity.name}
+ - Description: ${entity.description}
- Race: ${race.name}
- Location: ${location.name}
- Level: ${level.level} - ${level.experienceInLevel}/${level.experienceToNextLevel} exp
- - Vitality: ${player.vitality}
- - Strength: ${player.strength}
- - Endurance: ${player.endurance}
- - Agility: ${player.agility}
- - Dexterity: ${player.dexterity}
- - Intelligence: ${player.intelligence}
- - Wisdom: ${player.wisdom}
- - Stealth: ${player.stealth}
- - Charisma: ${player.charisma}
- - Lockpicking: ${player.lockpicking}
+ - Health: ${entity.health}/${getMaxHealth(entity)} (${getHealthPercentage(entity)}%)
+ - Vitality: ${entity.vitality}
+ - Strength: ${entity.strength}
+ - Endurance: ${entity.endurance}
+ - Agility: ${entity.agility}
+ - Dexterity: ${entity.dexterity}
+ - Intelligence: ${entity.intelligence}
+ - Wisdom: ${entity.wisdom}
+ - Stealth: ${entity.stealth}
+ - Charisma: ${entity.charisma}
+ - Lockpicking: ${entity.lockpicking}
`,
);
};
const onInventory = (text: string, roomId: string, sender: string) => {
- const player = getPlayerById(sender);
+ const player = getPlayer(sender);
const itemIndex = text.replace(`${gamePrefix} inventory `, "").trim();
const itemIndexInt = Number(itemIndex);
@@ -170,46 +193,52 @@ const onInventory = (text: string, roomId: string, sender: string) => {
};
const onLocation = (_text: string, roomId: string, sender: string) => {
- const player = getPlayerById(sender);
+ const player = getPlayer(sender);
const location = getLocation(player.location);
client.sendHtmlMessage(
roomId,
"",
- `
- - Player: ${player.name}
+ `Location info:
+
- X: ${location.X}
- Y: ${location.Y}
- Location Name: ${location.name}
-
- ${location.description}
`,
+ - Location Description: ${location.description}
+
`,
);
};
const onNearby = (_text: string, roomId: string, sender: string) => {
- const player = getPlayerById(sender);
+ const player = getPlayer(sender);
const location = getLocation(player.location);
+ const parentLocation = getParentLocation(player.location);
const mapLocation = (locId: Location): string => {
const locData = getLocation(locId);
const distance = getLocationDistance(location, locData);
- return `${locData.name} - ${distance.toFixed(1)}km - ${locData.description}`;
+ let distanceText = `${distance.toFixed(1)}km`;
+ if (locId === player.location) {
+ distanceText = `you are here`;
+ }
+
+ return `${locData.name} - ${distanceText} - ${locData.description}`;
};
client.sendHtmlMessage(
roomId,
"",
- `There are ${location.childLocations.length} locations around you (${player.name})
+ `There are ${location.childLocations.length + parentLocation.childLocations.length} locations around you (${player.name})
${location.childLocations.map(mapLocation).join("\n")}
- ${getParentLocation(player.location).childLocations.map(mapLocation).join("\n")}
+ ${parentLocation.childLocations.map(mapLocation).join("\n")}
`,
);
};
const onTravel = (text: string, roomId: string, sender: string) => {
- const player = getPlayerById(sender);
+ const player = getPlayer(sender);
const locationName = text.replace(`${gamePrefix} travel `, "").trim();
if (!existsLocation(locationName)) {
@@ -246,4 +275,44 @@ const onTravel = (text: string, roomId: string, sender: string) => {
startTravel(player, travelLocation.id, client, roomId);
};
+const onEntities = (_text: string, roomId: string, sender: string) => {
+ const player = getPlayer(sender);
+ const location = getLocation(player.location);
+
+ const mapEntity = (entity: IEntity): string => {
+ return `${entity.name} - ${getRace(entity.race).name}`;
+ };
+
+ client.sendHtmlMessage(
+ roomId,
+ "",
+ `Entities in ${location.name}:
+
+ ${getNpcsAtLocation(player.location).map(mapEntity)}
+
+ Players in ${location.name}:
+
+ ${getPlayersAtLocation(player.location).map(mapEntity)}
+
`,
+ );
+};
+
+const onEntity = (text: string, roomId: string, sender: string) => {
+ const player = getPlayer(sender);
+
+ const entityName = text.replace(`${gamePrefix} entity `, "").trim();
+ if (!existsEntity(entityName)) {
+ client.sendTextMessage(roomId, "No such entity exists");
+ return;
+ }
+
+ const entity = getEntityByName(entityName);
+ if (!entitiesShareLocation(player, entity)) {
+ client.sendTextMessage(roomId, "No such entity in your vicinity");
+ return;
+ }
+
+ onStatus("", roomId, sender);
+};
+
export { registerModuleGame };
diff --git a/src/services/game/entity.ts b/src/services/game/entity.ts
index 185cd00..49fa84c 100644
--- a/src/services/game/entity.ts
+++ b/src/services/game/entity.ts
@@ -1,6 +1,15 @@
import { getUserNameById } from "../../helpers.js";
import { state } from "../../store/store.js";
-import type { IEntity, IPlayer } from "./structures/entities.js";
+import {
+ npcBecky,
+ npcs,
+ type IEntity,
+ type INPC,
+ type INPCData,
+ type IPlayer,
+ type NPC,
+ type TFullNPC,
+} from "./structures/entities.js";
import { itemShortsword } from "./structures/items.js";
import { Location } from "./structures/locations.js";
import { Race, raceHuman, races, type IRace } from "./structures/races.js";
@@ -28,11 +37,36 @@ const createPlayer = (name: string): IPlayer => ({
lockpicking: 0,
});
-const getPlayerById = (userId: string): IPlayer => {
- return getPlayer(getUserNameById(userId));
+const createNpcData = (npc: INPC): INPCData => ({
+ id: npc.id,
+ health: getMaxHealth(npc),
+ dead: false,
+});
+
+const existsPlayer = (name: string): boolean => {
+ return (
+ state.game.players.find(
+ (player) => player.name.toLowerCase() === name.toLowerCase(),
+ ) !== undefined
+ );
};
-const getPlayer = (name: string): IPlayer => {
+const existsNpc = (name: string): boolean => {
+ return (
+ npcs.find((npc) => npc.name.toLowerCase() === name.toLowerCase()) !==
+ undefined
+ );
+};
+
+const existsEntity = (name: string): boolean => {
+ return existsPlayer(name) || existsNpc(name);
+};
+
+const getPlayer = (id: string): IPlayer => {
+ return getPlayer(getUserNameById(id));
+};
+
+const getPlayerByName = (name: string): IPlayer => {
const player = state.game.players.find((player) => player.name === name);
if (player) {
return player;
@@ -44,6 +78,55 @@ const getPlayer = (name: string): IPlayer => {
return newPlayer;
};
+const getEntityByName = (name: string): IPlayer | TFullNPC => {
+ if (existsPlayer(name)) {
+ return getPlayer(name);
+ }
+
+ const npc = getNpcByName(name);
+
+ return { ...npc, ...getNpcData(npc.id) };
+};
+
+const getNpc = (id: NPC): INPC => {
+ const npc = npcs.find((npc) => npc.id === id);
+
+ return npc ? npc : npcBecky;
+};
+
+const getNpcByName = (name: string): INPC => {
+ const npc = npcs.find((npc) => npc.id === name);
+
+ return npc ? npc : npcBecky;
+};
+
+const getNpcData = (id: NPC): INPCData => {
+ const npcData = state.game.npcs.find((npcData) => npcData.id === id);
+ if (npcData) {
+ return npcData;
+ }
+
+ const newNpcData = createNpcData(getNpc(id));
+ state.game.npcs.push(newNpcData);
+
+ return newNpcData;
+};
+
+const getNpcsAtLocation = (id: Location): INPC[] => {
+ return npcs.filter((npc) => npc.location === id);
+};
+
+const getPlayersAtLocation = (id: Location): IPlayer[] => {
+ return state.game.players.filter((player) => player.location === id);
+};
+
+const getEntitiesAtLocation = (id: Location): IEntity[] => {
+ const npcsAtLocation = getNpcsAtLocation(id);
+ const playersAtLocation = getPlayersAtLocation(id);
+
+ return [...npcsAtLocation, ...playersAtLocation];
+};
+
const getRace = (id: Race): IRace => {
const race = races.find((race) => race.id === id);
@@ -69,8 +152,41 @@ const getLevel = (experience: number): ILevel => {
};
};
-const getSpeed = (entity: IEntity) => {
- return 1 + Math.sqrt(entity.agility + entity.endurance) / 3;
+const getMaxHealth = (entity: IEntity): number => {
+ return entity.vitality * 10;
};
-export { createPlayer, getPlayerById, getPlayer, getRace, getLevel, getSpeed };
+const getHealthPercentage = (entity: IPlayer | INPC): number => {
+ if ("health" in entity) {
+ return entity.health / getMaxHealth(entity);
+ }
+
+ const npcData = getNpcData(entity.id);
+ return npcData.health / getMaxHealth(entity);
+};
+
+const getSpeed = (entity: IEntity) => {
+ return 60 + 3 + Math.sqrt(entity.agility + entity.endurance) / 2;
+};
+
+export {
+ createPlayer,
+ createNpcData,
+ existsPlayer,
+ existsNpc,
+ existsEntity,
+ getPlayer,
+ getPlayerByName,
+ getEntityByName,
+ getNpc,
+ getNpcByName,
+ getNpcData,
+ getNpcsAtLocation,
+ getPlayersAtLocation,
+ getEntitiesAtLocation,
+ getRace,
+ getLevel,
+ getMaxHealth,
+ getHealthPercentage,
+ getSpeed,
+};
diff --git a/src/services/game/location.ts b/src/services/game/location.ts
index f0881cc..dbb7939 100644
--- a/src/services/game/location.ts
+++ b/src/services/game/location.ts
@@ -5,7 +5,7 @@ import {
locations,
type ILocation,
} from "./structures/locations.js";
-import type { IPlayer } from "./structures/entities.js";
+import type { IEntity, IPlayer } from "./structures/entities.js";
import { getSpeed } from "./entity.js";
const existsLocation = (name: string): boolean => {
@@ -34,8 +34,24 @@ const getLocationDistance = (
locationA: ILocation,
locationB: ILocation,
): number => {
- const deltaX = Math.abs(locationA.X - locationB.X);
- const deltaY = Math.abs(locationA.Y - locationB.Y);
+ const parentOfA = getParentLocation(locationA.id);
+ const parentOfB = getParentLocation(locationB.id);
+
+ let locAX = locationA.X;
+ let locAY = locationA.Y;
+ let locBX = locationB.X;
+ let locBY = locationB.Y;
+
+ if (locationA.id === parentOfB.id) {
+ locBX += locAX;
+ locBY += locAY;
+ } else if (locationB.id === parentOfA.id) {
+ locAX += locBX;
+ locAY += locBY;
+ }
+
+ const deltaX = Math.abs(locAX - locBX);
+ const deltaY = Math.abs(locAY - locBY);
const deltaPow = Math.pow(deltaX, 2) + Math.pow(deltaY, 2);
@@ -59,6 +75,13 @@ const hasLocation = (
return childLocations.includes(locationChild);
};
+const entitiesShareLocation = (entityA: IEntity, entityB: IEntity): boolean => {
+ const locationA = getLocation(entityA.location);
+ const locationB = getLocation(entityB.location);
+
+ return locationA.id === locationB.id;
+};
+
const getTravelTimeInHours = (
player: IPlayer,
locationA: ILocation,
@@ -115,6 +138,7 @@ export {
getLocationDistance,
getParentLocation,
hasLocation,
+ entitiesShareLocation,
getTravelTimeInHours,
getTravelTimeInMilliseconds,
startTravel,
diff --git a/src/services/game/structures/entities.ts b/src/services/game/structures/entities.ts
index d753dd2..c9d63b7 100644
--- a/src/services/game/structures/entities.ts
+++ b/src/services/game/structures/entities.ts
@@ -36,6 +36,8 @@ export interface INPCData {
dead: boolean;
}
+export type TFullNPC = INPC & INPCData;
+
export enum NPC {
BECKY = "BECKY",
}