This commit is contained in:
Aslan 2026-01-20 19:54:45 -05:00
parent d36e98ad0b
commit c071b286af
23 changed files with 713 additions and 11 deletions

View file

@ -80,6 +80,18 @@ const getUserName = (user: IUser): string => {
return username;
};
const getUserNameById = (userId: string): string => {
const userPattern = /@[a-zA-Z0-9]*/;
const match = userId.match(userPattern)?.at(0);
if (!match) {
return "";
}
let username = match.replaceAll("@", "");
username = username.charAt(0).toUpperCase() + username.slice(1);
return username;
};
const getUserFromMention = (mention: string | undefined): IUser | undefined => {
if (!mention) {
return undefined;
@ -139,6 +151,7 @@ export {
checkRoles,
getLevel,
getUserName,
getUserNameById,
getUserFromMention,
changePersonality,
log,

View file

@ -36,6 +36,7 @@ const onAI = async (
repliedMessage?: string,
repliedSender?: string,
image?: Buffer<ArrayBuffer>,
repliedImage?: Buffer<ArrayBuffer>,
) => {
if (text.startsWith(`${config.app.triggerPrefix}aileaderboard`)) {
return;
@ -75,6 +76,7 @@ const onAI = async (
`${username}: ${textMod}`,
`${repliedUsername}: ${repliedMessage}`,
image,
repliedImage,
);
user.aiCost += responseAI.tokens * prices.text;

153
src/modules/game/game.ts Normal file
View file

@ -0,0 +1,153 @@
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 {
getLocation,
getLocationDistance,
} from "../../services/game/location.js";
import { getLevel } from "../../services/game/game.js";
import {
getFullItemName,
type IItem,
type Location,
} from "../../services/game/index.js";
let client: MatrixClient;
const gamePrefix = `${config.app.triggerPrefix}game`;
const registerModuleGame = (
matrixClient: MatrixClient,
callbackStore: ICallbackStore,
) => {
client = matrixClient;
callbackStore.messageCallbacks.push({
startConditions: [`${gamePrefix} help`],
callbackFunc: onHelp,
});
callbackStore.messageCallbacks.push({
startConditions: [`${gamePrefix} status`],
callbackFunc: onStatus,
});
callbackStore.messageCallbacks.push({
startConditions: [`${gamePrefix} inventory`],
callbackFunc: onInventory,
});
callbackStore.messageCallbacks.push({
startConditions: [`${gamePrefix} location`],
callbackFunc: onLocation,
});
callbackStore.messageCallbacks.push({
startConditions: [`${gamePrefix} locations`],
callbackFunc: onLocations,
});
};
const onHelp = (_text: string, roomId: string) => {
client.sendHtmlMessage(
roomId,
"",
`<ul>
<li><b>!game help</b> - Prints this help message</li>
<li><b>!game status</b> - Prints information about your character</li>
<li><b>!game inventory</b> - Shows your inventory</li>
<li><b>(WIP) !game inventory {index}</b> - Shows information about an item in your inventory</li>
<li><b>!game location</b> - Shows information about your current location</li>
<li><b>!game locations</b> - Shows nearby locations</li>
<li><b>(WIP) !game travel {location}</b> - Travel to a location</li>
<li><b>(WIP) !game talk {entity}</b> - Talk to an entity</li>
<li><b>(WIP) !game fight {entity}</b> - Fight an entity</li>
<li><b>(WIP) !game work {action}</b> - Start work</li>
</ul>`,
);
};
const onStatus = (_text: string, roomId: string, sender: string) => {
const player = getPlayerById(sender);
const race = getRace(player.race);
const location = getLocation(player.location);
const level = getLevel(player.experience);
client.sendHtmlMessage(
roomId,
"",
`<ul>
<li><b>Name:</b> ${player.name}</li>
<li><b>Description:</b> ${player.description}</li>
<li><b>Race:</b> ${race.name}</li>
<li><b>Location:</b> ${location.name}</li>
<li><b>Level:</b> ${level.level} - ${level.experienceInLevel}/${level.experienceToNextLevel}</li>
<li><b>Vitality:</b> ${player.vitality}</li>
<li><b>Strength:</b> ${player.strength}</li>
<li><b>Endurance:</b> ${player.endurance}</li>
<li><b>Agility:</b> ${player.agility}</li>
<li><b>Dexterity:</b> ${player.dexterity}</li>
<li><b>Intelligence:</b> ${player.intelligence}</li>
<li><b>Wisdom:</b> ${player.wisdom}</li>
<li><b>Stealth:</b> ${player.stealth}</li>
<li><b>Charisma:</b> ${player.charisma}</li>
<li><b>Lockpicking:</b> ${player.lockpicking}</li>
</ul>`,
);
};
const onInventory = (_text: string, roomId: string, sender: string) => {
const player = getPlayerById(sender);
const mapItem = (item: IItem, index: number): string => {
const fullName = getFullItemName(item);
return `<b>(${index})</b> ${fullName}, `;
};
client.sendHtmlMessage(
roomId,
"",
`<p>Your inventory (${player.name})</p>
<ul>
${player.inventory.items.map(mapItem)}
</ul>`,
);
};
const onLocation = (_text: string, roomId: string, sender: string) => {
const player = getPlayerById(sender);
const location = getLocation(player.location);
client.sendHtmlMessage(
roomId,
"",
`<ul>
<li><b>Player:</b> ${player.name}</li>
<li><b>X:</b> ${location.X}</li>
<li><b>Y:</b> ${location.Y}</li>
<li><b>Location Name:</b> ${location.name}</li>
</ul>
<p>${location.description}</p>`,
);
};
const onLocations = (_text: string, roomId: string, sender: string) => {
const player = getPlayerById(sender);
const location = getLocation(player.location);
const mapLocation = (locId: Location): string => {
const locData = getLocation(locId);
const distance = getLocationDistance(location, locData);
return `<li><b>${locData.name}</b> - ${distance.toFixed(1)}km - <i>${locData.description}</i></li>`;
};
client.sendHtmlMessage(
roomId,
"",
`<p>There are ${location.childLocations.length} locations around you (${player.name})</p>
<ul>
${location.childLocations.map(mapLocation)}
</ul>`,
);
};
export { registerModuleGame };

View file

@ -0,0 +1 @@
export * from "./game.js";

View file

@ -10,6 +10,7 @@ import { registerModuleBase } from "./base/base.js";
import { registerModuleAdmin } from "./admin/admin.js";
import { registerModuleUser } from "./user/user.js";
import { registerModuleAI } from "./ai/ai.js";
import { registerModuleGame } from "./game/game.js";
import { checkRoles, getUserById, log } from "../helpers.js";
import { onAnyMessage, onMissingRole } from "./global.js";
import { config } from "../config.js";
@ -113,6 +114,7 @@ const registerModules = (client: MatrixClient) => {
const replyToId = relatesTo?.["m.in_reply_to"]?.event_id;
let repliedMessage: string | undefined;
let repliedSender: string | undefined;
let repliedImage: Buffer<ArrayBuffer> | undefined;
if (replyToId) {
const repliedEvent = await client.fetchRoomEvent(roomId, replyToId);
@ -125,6 +127,32 @@ const registerModules = (client: MatrixClient) => {
} else if (repliedContent.formatted_body) {
repliedMessage = repliedContent.formatted_body.toString();
}
if (repliedContent.msgtype === MsgType.Image) {
if (typeof content.url === "string") {
const httpUrl = client.mxcUrlToHttp(
content.url,
undefined,
undefined,
undefined,
undefined,
true,
true,
);
if (httpUrl) {
const imageResponse = await fetch(httpUrl, {
headers: {
Authorization: `Bearer ${client.getAccessToken()}`,
},
});
const arrayBuffer =
await imageResponse.arrayBuffer();
repliedImage = Buffer.from(
new Uint8Array(arrayBuffer),
);
}
}
}
}
}
@ -151,6 +179,7 @@ const registerModules = (client: MatrixClient) => {
repliedMessage,
repliedSender,
image,
repliedImage,
);
}
});
@ -160,6 +189,7 @@ const registerModules = (client: MatrixClient) => {
registerModuleAdmin(client, callbacks);
registerModuleUser(client, callbacks);
registerModuleAI(client, callbacks);
registerModuleGame(client, callbacks);
};
export { registerModules };

View file

@ -17,6 +17,7 @@ interface ICallback {
repliedMessage?: string,
repliedSender?: string,
image?: Buffer<ArrayBuffer>,
repliedImage?: Buffer<ArrayBuffer>,
) => void;
}

View file

@ -23,17 +23,34 @@ const getTextGemini = async (
input: string,
oldInput?: string,
inputImage?: Buffer<ArrayBuffer>,
oldInputImage?: Buffer<ArrayBuffer>,
): Promise<AIResponseText> => {
log(`AI Text Request: ${input}`);
const oldInputContent: Content = {
role: "user",
parts: [
{
text: oldInput ?? "",
},
],
};
const oldInputContent: Content = oldInputImage
? {
role: "user",
parts: [
{
text: oldInput ?? "",
},
{
inlineData: {
mimeType: "image/png",
data: Buffer.from(oldInputImage).toString("base64"),
},
},
],
}
: {
role: "user",
parts: [
{
text: oldInput ?? "",
},
],
};
const inputContent: Content = inputImage
? {
role: "user",

View file

@ -0,0 +1,52 @@
import { getUserNameById } from "../../helpers.js";
import { state } from "../../store/store.js";
import type { IPlayer } 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";
const createPlayer = (name: string): IPlayer => ({
name: name,
description: "",
race: Race.HUMAN,
location: Location.FARLANDS,
inventory: {
items: [itemShortsword],
},
experience: 0,
health: 100,
vitality: 0,
strength: 0,
endurance: 0,
agility: 0,
dexterity: 0,
intelligence: 0,
wisdom: 0,
stealth: 0,
charisma: 0,
lockpicking: 0,
});
const getPlayerById = (userId: string): IPlayer => {
return getPlayer(getUserNameById(userId));
};
const getPlayer = (name: string): IPlayer => {
const player = state.game.players.find((player) => player.name === name);
if (player) {
return player;
}
const newPlayer = createPlayer(name);
state.game.players.push(newPlayer);
return newPlayer;
};
const getRace = (id: Race): IRace => {
const race = races.find((race) => race.id === id);
return race ? race : raceHuman;
};
export { createPlayer, getPlayerById, getPlayer, getRace };

22
src/services/game/game.ts Normal file
View file

@ -0,0 +1,22 @@
import type { ILevel } from "./types.js";
const getLevel = (experience: number): ILevel => {
let tmpExperience = experience;
let experienceToNextLevel = 50;
let level = 0;
while (tmpExperience >= experienceToNextLevel) {
level++;
tmpExperience -= experienceToNextLevel;
experienceToNextLevel = experienceToNextLevel *= 1.25;
}
return {
level: level,
totalExperience: Math.floor(experience),
experienceInLevel: Math.floor(tmpExperience),
experienceToNextLevel: Math.floor(experienceToNextLevel),
};
};
export { getLevel };

View file

@ -0,0 +1,11 @@
export * from "./game.js";
export * from "./entity.js";
export * from "./location.js";
export * from "./item.js";
export * from "./types.js";
export * from "./structures/locations.js";
export * from "./structures/entities.js";
export * from "./structures/races.js";
export * from "./structures/items.js";
export * from "./structures/rarities.js";
export * from "./structures/quests.js";

23
src/services/game/item.ts Normal file
View file

@ -0,0 +1,23 @@
import type { IItem } from "./structures/items.js";
import {
rarities,
Rarity,
rarityCommon,
type IRarity,
} from "./structures/rarities.js";
const getFullItemName = (item: IItem): string => {
return `${getRarityName(item.rarity)} ${item.baseName}`;
};
const getRarityName = (id: Rarity): string => {
return getRarity(id).name;
};
const getRarity = (id: Rarity): IRarity => {
const rarity = rarities.find((rarity) => rarity.id === id);
return rarity ? rarity : rarityCommon;
};
export { getFullItemName, getRarityName, getRarity };

View file

@ -0,0 +1,26 @@
import {
locationFarlands,
locations,
type ILocation,
type Location,
} from "./structures/locations.js";
const getLocation = (id: Location): ILocation => {
const location = locations.find((location) => location.id === id);
return location ? location : locationFarlands;
};
const getLocationDistance = (
locationA: ILocation,
locationB: ILocation,
): number => {
const deltaX = Math.abs(locationA.X - locationB.X);
const deltaY = Math.abs(locationA.Y - locationB.Y);
const deltaPow = Math.pow(deltaX, 2) + Math.pow(deltaY, 2);
return Math.sqrt(deltaPow);
};
export { getLocation, getLocationDistance };

View file

@ -0,0 +1,72 @@
import { Location } from "./locations.js";
import { Race } from "./races.js";
import type { IInventory } from "../types.js";
export interface IEntity {
name: string;
description: string;
race: Race;
location: Location;
inventory: IInventory;
experience: number;
vitality: number;
strength: number;
endurance: number;
agility: number;
dexterity: number;
intelligence: number;
wisdom: number;
stealth: number;
charisma: number;
lockpicking: number;
}
export interface IPlayer extends IEntity {
health: number;
}
export interface INPC extends IEntity {
id: NPC;
type: NpcType;
}
export interface INPCData {
id: NPC;
health: number;
dead: boolean;
}
export enum NPC {
BECKY = "BECKY",
}
export enum NpcType {
QUEST_GIVER = "QUEST_GIVER",
PASSIVE = "PASSIVE",
AGGRESIVE = "AGGRESIVE",
}
export const npcBecky: INPC = {
id: NPC.BECKY,
name: "Becky",
description: "A 50 meter tall giantess. Might be a bad idea to attack",
race: Race.GIANT,
location: Location.NIGHTROOT_FOREST,
inventory: {
items: [],
},
experience: 10000,
vitality: 250,
strength: 250,
endurance: 50,
agility: 5,
dexterity: 10,
intelligence: 20,
wisdom: 30,
stealth: 0,
charisma: 5,
lockpicking: 50,
type: NpcType.QUEST_GIVER,
};
export const npcs = [npcBecky];

View file

@ -0,0 +1,59 @@
import { Rarity } from "./rarities.js";
export interface IItem {
baseName: string;
description: string;
value: number;
durability: number;
type: ItemType;
rarity: Rarity;
stats: IStat[];
}
export interface IStat {
abilityType?: Ability;
damageType?: DamageType;
damage: number;
defense: number;
}
export enum ItemType {
ITEM = "ITEM",
CURRENCY = "CURRENCY",
WEAPON_1H = "WEAPON_1H",
WEAPON_2H = "WEAPON_2H",
SHIELD = "SHIELD",
CHEST = "CHEST",
LEGS = "LEGS",
HEAD = "HEAD",
BOOTS = "BOOTS",
}
export enum DamageType {
PHYSICAL = "PHYSICAL",
ELEMENTAL = "ELEMENTAL",
ARCANE = "ARCANE",
PSYCHIC = "PSYCHIC",
POISON = "POISON",
RADIATION = "RADIATION",
}
export enum Ability {
TELEPORT = "TELEPORT",
}
export const itemShortsword: IItem = {
baseName: "Shortsword",
description: "A common sword",
value: 10,
durability: 100,
type: ItemType.WEAPON_1H,
rarity: Rarity.COMMON,
stats: [
{
damageType: DamageType.PHYSICAL,
damage: 10,
defense: 0,
},
],
};

View file

@ -0,0 +1,97 @@
export interface ILocation {
id: Location;
name: string;
description: string;
X: number;
Y: number;
childLocations: Location[];
}
export interface ILocationData {
id: Location;
destroyed: boolean;
}
export enum Location {
UNIVERSE = "UNIVERSE",
SOL = "SOL",
EARTH = "EARTH",
FARLANDS = "FARLANDS",
HIGHMERE = "HIGHMERE",
HIGHMERE_TAVERN = "HIGHMERE_TAVERN",
NIGHTROOT_FOREST = "NIGHTROOT_FOREST",
}
export const locationUniverse: ILocation = {
id: Location.UNIVERSE,
name: "Universe",
description: "Everything",
X: 0,
Y: 0,
childLocations: [Location.SOL],
};
export const locationSol: ILocation = {
id: Location.SOL,
name: "Sol",
description: "Home system",
X: 0,
Y: 0,
childLocations: [Location.EARTH],
};
export const locationEarth: ILocation = {
id: Location.EARTH,
name: "Earth",
description: "Home planet",
X: 0,
Y: 0,
childLocations: [Location.FARLANDS],
};
export const locationFarlands: ILocation = {
id: Location.FARLANDS,
name: "Farlands",
description: "Large plains",
X: 0,
Y: 0,
childLocations: [Location.HIGHMERE, Location.NIGHTROOT_FOREST],
};
export const locationHighmere: ILocation = {
id: Location.HIGHMERE,
name: "Highmere",
description: "A large capital city of Farlands",
X: 5,
Y: 5,
childLocations: [Location.HIGHMERE_TAVERN],
};
export const locationHighmereTavern: ILocation = {
id: Location.HIGHMERE_TAVERN,
name: "Highmere Tavern",
description: "",
X: 0.3,
Y: 0.5,
childLocations: [],
};
export const locationNightrootForest: ILocation = {
id: Location.NIGHTROOT_FOREST,
name: "Nightroot Forest",
description:
"Forest full of tall trees through which barely any light shines",
X: 3,
Y: 6,
childLocations: [],
};
export const locations = [
locationUniverse,
locationSol,
locationEarth,
locationFarlands,
locationHighmere,
locationHighmereTavern,
locationNightrootForest,
];

View file

@ -0,0 +1,10 @@
export interface IQuest {
name: string;
description: string;
type: QuestType;
}
export enum QuestType {
FETCH = "FETCH",
KILL = "KILL",
}

View file

@ -0,0 +1,31 @@
export interface IRace {
id: Race;
name: string;
description: string;
}
export enum Race {
HUMAN = "HUMAN",
GIANT = "GIANT",
ELF = "ELF",
}
export const raceHuman: IRace = {
id: Race.HUMAN,
name: "Human",
description: "Just a normal human",
};
export const raceGiant: IRace = {
id: Race.GIANT,
name: "Giant",
description: "Basically a human, but of an extreme size and strength",
};
export const raceElf: IRace = {
id: Race.ELF,
name: "Elf",
description: "A weird creature with pointy ears",
};
export const races = [raceHuman, raceGiant, raceElf];

View file

@ -0,0 +1,52 @@
export interface IRarity {
id: Rarity;
name: string;
}
export enum Rarity {
COMMON = "COMMON",
UNCOMMON = "UNCOMMON",
RARE = "RARE",
VERY_RARE = "VERY_RARE",
LEGENDARY = "LEGENDARY",
MAGICAL = "MAGICAL",
}
export const rarityCommon: IRarity = {
id: Rarity.COMMON,
name: "Common",
};
export const rarityUncommon: IRarity = {
id: Rarity.UNCOMMON,
name: "Uncommon",
};
export const rarityRare: IRarity = {
id: Rarity.RARE,
name: "Rare",
};
export const rarityVeryRare: IRarity = {
id: Rarity.VERY_RARE,
name: "Very Rare",
};
export const rarityLegendary: IRarity = {
id: Rarity.LEGENDARY,
name: "Legendary",
};
export const rarityMagical: IRarity = {
id: Rarity.MAGICAL,
name: "Magical",
};
export const rarities = [
rarityCommon,
rarityUncommon,
rarityRare,
rarityVeryRare,
rarityLegendary,
rarityMagical,
];

View file

@ -0,0 +1,22 @@
import type { IItem } from "./structures/items.js";
import type { ILocationData } from "./structures/locations.js";
import type { INPCData, IPlayer } from "./structures/entities.js";
interface IGame {
players: IPlayer[];
npcs: INPCData[];
locations: ILocationData[];
}
interface IInventory {
items: IItem[];
}
interface ILevel {
level: number;
totalExperience: number;
experienceInLevel: number;
experienceToNextLevel: number;
}
export { type IGame, type IInventory, type ILevel };

View file

@ -11,6 +11,11 @@ let state: IState = {
startTime: 0,
},
aiMemory: [],
game: {
players: [],
npcs: [],
locations: [],
},
};
const load = () => {

View file

@ -1,7 +1,10 @@
import type { IGame } from "../services/game/types.js";
interface IState {
users: IUser[];
personality: IPersonality;
aiMemory: string[];
game: IGame;
}
interface IUser {