diff --git a/package-lock.json b/package-lock.json index 2d78344..50ca266 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aslobot-matrix", - "version": "1.2.3", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aslobot-matrix", - "version": "1.2.3", + "version": "1.3.0", "license": "ISC", "dependencies": { "@google/genai": "^1.34.0", diff --git a/package.json b/package.json index 7ad3718..51ace47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aslobot-matrix", - "version": "1.2.3", + "version": "1.3.0", "description": "", "license": "ISC", "author": "", diff --git a/src/helpers.ts b/src/helpers.ts index 2af3a36..1fee0c4 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -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, diff --git a/src/modules/ai/ai.ts b/src/modules/ai/ai.ts index 167977f..ef072f4 100644 --- a/src/modules/ai/ai.ts +++ b/src/modules/ai/ai.ts @@ -36,6 +36,7 @@ const onAI = async ( repliedMessage?: string, repliedSender?: string, image?: Buffer, + repliedImage?: Buffer, ) => { 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; diff --git a/src/modules/game/game.ts b/src/modules/game/game.ts new file mode 100644 index 0000000..543ec50 --- /dev/null +++ b/src/modules/game/game.ts @@ -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, + "", + ``, + ); +}; + +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, + "", + ``, + ); +}; + +const onInventory = (_text: string, roomId: string, sender: string) => { + const player = getPlayerById(sender); + + const mapItem = (item: IItem, index: number): string => { + const fullName = getFullItemName(item); + + return `(${index}) ${fullName}, `; + }; + + client.sendHtmlMessage( + roomId, + "", + `

Your inventory (${player.name})

+ `, + ); +}; + +const onLocation = (_text: string, roomId: string, sender: string) => { + const player = getPlayerById(sender); + const location = getLocation(player.location); + + client.sendHtmlMessage( + roomId, + "", + ` +

${location.description}

`, + ); +}; + +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 `
  • ${locData.name} - ${distance.toFixed(1)}km - ${locData.description}
  • `; + }; + + client.sendHtmlMessage( + roomId, + "", + `

    There are ${location.childLocations.length} locations around you (${player.name})

    + `, + ); +}; + +export { registerModuleGame }; diff --git a/src/modules/game/index.ts b/src/modules/game/index.ts new file mode 100644 index 0000000..5217113 --- /dev/null +++ b/src/modules/game/index.ts @@ -0,0 +1 @@ +export * from "./game.js"; diff --git a/src/modules/module.ts b/src/modules/module.ts index 70c0751..c67f2cb 100644 --- a/src/modules/module.ts +++ b/src/modules/module.ts @@ -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 | 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 }; diff --git a/src/modules/types.ts b/src/modules/types.ts index 2ddd14d..8c0cd6b 100644 --- a/src/modules/types.ts +++ b/src/modules/types.ts @@ -17,6 +17,7 @@ interface ICallback { repliedMessage?: string, repliedSender?: string, image?: Buffer, + repliedImage?: Buffer, ) => void; } diff --git a/src/services/ai/ai.ts b/src/services/ai/ai.ts index b3c54ee..157d51f 100644 --- a/src/services/ai/ai.ts +++ b/src/services/ai/ai.ts @@ -23,17 +23,34 @@ const getTextGemini = async ( input: string, oldInput?: string, inputImage?: Buffer, + oldInputImage?: Buffer, ): Promise => { 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", diff --git a/src/services/game/entity.ts b/src/services/game/entity.ts new file mode 100644 index 0000000..80973e0 --- /dev/null +++ b/src/services/game/entity.ts @@ -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 }; diff --git a/src/services/game/game.ts b/src/services/game/game.ts new file mode 100644 index 0000000..6e64232 --- /dev/null +++ b/src/services/game/game.ts @@ -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 }; diff --git a/src/services/game/index.ts b/src/services/game/index.ts new file mode 100644 index 0000000..9ac0521 --- /dev/null +++ b/src/services/game/index.ts @@ -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"; diff --git a/src/services/game/item.ts b/src/services/game/item.ts new file mode 100644 index 0000000..b2cd4e7 --- /dev/null +++ b/src/services/game/item.ts @@ -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 }; diff --git a/src/services/game/location.ts b/src/services/game/location.ts new file mode 100644 index 0000000..9e1b556 --- /dev/null +++ b/src/services/game/location.ts @@ -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 }; diff --git a/src/services/game/structures/entities.ts b/src/services/game/structures/entities.ts new file mode 100644 index 0000000..d753dd2 --- /dev/null +++ b/src/services/game/structures/entities.ts @@ -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]; diff --git a/src/services/game/structures/items.ts b/src/services/game/structures/items.ts new file mode 100644 index 0000000..bf00f11 --- /dev/null +++ b/src/services/game/structures/items.ts @@ -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, + }, + ], +}; diff --git a/src/services/game/structures/locations.ts b/src/services/game/structures/locations.ts new file mode 100644 index 0000000..59d44d9 --- /dev/null +++ b/src/services/game/structures/locations.ts @@ -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, +]; diff --git a/src/services/game/structures/quests.ts b/src/services/game/structures/quests.ts new file mode 100644 index 0000000..7a5cb47 --- /dev/null +++ b/src/services/game/structures/quests.ts @@ -0,0 +1,10 @@ +export interface IQuest { + name: string; + description: string; + type: QuestType; +} + +export enum QuestType { + FETCH = "FETCH", + KILL = "KILL", +} diff --git a/src/services/game/structures/races.ts b/src/services/game/structures/races.ts new file mode 100644 index 0000000..1e6349e --- /dev/null +++ b/src/services/game/structures/races.ts @@ -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]; diff --git a/src/services/game/structures/rarities.ts b/src/services/game/structures/rarities.ts new file mode 100644 index 0000000..ca858fe --- /dev/null +++ b/src/services/game/structures/rarities.ts @@ -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, +]; diff --git a/src/services/game/types.ts b/src/services/game/types.ts new file mode 100644 index 0000000..fa23fe0 --- /dev/null +++ b/src/services/game/types.ts @@ -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 }; diff --git a/src/store/store.ts b/src/store/store.ts index 6afdce6..50ca33d 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -11,6 +11,11 @@ let state: IState = { startTime: 0, }, aiMemory: [], + game: { + players: [], + npcs: [], + locations: [], + }, }; const load = () => { diff --git a/src/store/types.ts b/src/store/types.ts index 5604fa3..d7accd2 100644 --- a/src/store/types.ts +++ b/src/store/types.ts @@ -1,7 +1,10 @@ +import type { IGame } from "../services/game/types.js"; + interface IState { users: IUser[]; personality: IPersonality; aiMemory: string[]; + game: IGame; } interface IUser {