diff --git a/src/modules/game/game.ts b/src/modules/game/game.ts index 2a10536..9efa74e 100644 --- a/src/modules/game/game.ts +++ b/src/modules/game/game.ts @@ -25,6 +25,7 @@ import { } from "../../services/game/location.js"; import { getLevel } from "../../services/game/entity.js"; import { + fightEntity, getFullItemName, getTotalStats, type IEntity, @@ -79,6 +80,10 @@ const registerModuleGame = ( startConditions: [`${gamePrefix} entity `], callbackFunc: onEntity, }); + callbackStore.messageCallbacks.push({ + startConditions: [`${gamePrefix} fight `], + callbackFunc: onFight, + }); }; const onHelp = (_text: string, roomId: string) => { @@ -343,4 +348,22 @@ const onEntity = (text: string, roomId: string, sender: string) => { onStatusName(roomId, entity.name); }; +const onFight = (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; + } + + fightEntity(client, roomId, player, entity); +}; + export { registerModuleGame }; diff --git a/src/services/game/entity.ts b/src/services/game/entity.ts index b5ba05b..e464fd1 100644 --- a/src/services/game/entity.ts +++ b/src/services/game/entity.ts @@ -1,8 +1,12 @@ import { getUserNameById } from "../../helpers.js"; import { state } from "../../store/store.js"; import { + Attack, + attackPunch, + attacks, npcBecky, npcs, + type IAttack, type IEntity, type INPC, type INPCData, @@ -16,6 +20,7 @@ import { Race, raceHuman, races, type IRace } from "./structures/races.js"; import type { ILevel } from "./types.js"; const createPlayer = (name: string): IPlayer => ({ + isPlayer: true, name: name, description: "", race: Race.HUMAN, @@ -35,6 +40,7 @@ const createPlayer = (name: string): IPlayer => ({ stealth: 0, charisma: 0, lockpicking: 0, + attacks: [Attack.PUNCH, Attack.KICK], }); const createNpcData = (npc: INPC): INPCData => ({ @@ -43,6 +49,10 @@ const createNpcData = (npc: INPC): INPCData => ({ dead: false, }); +const isPlayer = (entity: IEntity): boolean => { + return "isPlayer" in entity; +}; + const existsPlayer = (name: string): boolean => { return ( state.game.players.find( @@ -62,6 +72,12 @@ const existsEntity = (name: string): boolean => { return existsPlayer(name) || existsNpc(name); }; +const getAttack = (id: Attack): IAttack => { + const attack = attacks.find((attack) => attack.id === id); + + return attack ? attack : attackPunch; +}; + const getPlayer = (userId: string): IPlayer => { return getPlayerByName(getUserNameById(userId)); }; @@ -176,9 +192,11 @@ const getSpeed = (entity: IEntity) => { export { createPlayer, createNpcData, + isPlayer, existsPlayer, existsNpc, existsEntity, + getAttack, getPlayer, getPlayerByName, getEntityByName, diff --git a/src/services/game/game.ts b/src/services/game/game.ts index 473a0f4..7f70916 100644 --- a/src/services/game/game.ts +++ b/src/services/game/game.ts @@ -0,0 +1,160 @@ +import type { MatrixClient } from "matrix-js-sdk"; +import type { + Attack, + IEntity, + INPC, + IPlayer, + TFullNPC, +} from "./structures/entities.js"; +import { locationFarlands } from "./structures/locations.js"; +import { getAttack, getMaxHealth, getNpcData, isPlayer } from "./entity.js"; +import { sleep } from "matrix-js-sdk/lib/utils.js"; + +const fightEntity = async ( + client: MatrixClient, + roomId: string, + attacker: IPlayer | TFullNPC, + defender: IPlayer | TFullNPC, +) => { + let attackerAttacks: Attack[] = []; + let attackerDamage = 0; + let attackerDefense = 0; + attacker.inventory.items.forEach((item) => { + item.attacks.forEach((attack) => { + attackerAttacks.push(attack); + }); + item.stats.forEach((stat) => { + attackerDamage += stat.damage; + attackerDefense += stat.defense; + }); + }); + attackerDamage *= 1 + attacker.strength / 10; + attackerDefense *= 1 + attacker.strength / 10; + + let defenderAttacks: Attack[] = []; + let defenderDamage = 0; + let defenderDefense = 0; + defender.inventory.items.forEach((item) => { + item.attacks.forEach((attack) => { + defenderAttacks.push(attack); + }); + item.stats.forEach((stat) => { + defenderDamage += stat.damage; + defenderDefense += stat.defense; + }); + }); + defenderDamage *= 1 + defender.strength / 10; + defenderDefense *= 1 + defender.strength / 10; + + let winner: IPlayer | TFullNPC | undefined = undefined; + let loser: IPlayer | TFullNPC | undefined = undefined; + + while (true) { + await sleep(5000); + + const attackerWon = fightRound( + client, + roomId, + attackerAttacks, + attackerDamage, + defender, + defenderDefense, + ); + + if (attackerWon) { + winner = attacker; + loser = defender; + break; + } + + const defenderWon = fightRound( + client, + roomId, + defenderAttacks, + defenderDamage, + attacker, + attackerDefense, + ); + + if (defenderWon) { + winner = defender; + loser = attacker; + break; + } + } + + client.sendTextMessage( + roomId, + `${attacker.name} has won the fight against ${defender.name}!`, + ); + + if (isPlayer(loser)) { + respawnPlayer(client, roomId, loser as IPlayer); + } else { + const npcData = getNpcData((loser as INPC).id); + npcData.dead = true; + } +}; + +const fightRound = ( + client: MatrixClient, + roomId: string, + attackerAttacks: Attack[], + attackerDamage: number, + defender: IPlayer | TFullNPC, + defenderDefense: number, +): boolean => { + const attackerAttack = + attackerAttacks[Math.floor(Math.random() * attackerAttacks.length)]; + + if (!attackerAttack) { + return false; + } + + const attackerAttackInfo = getAttack(attackerAttack); + + const attackerAttackDamage = + attackerDamage * + attackerAttackInfo.damageMultiplier * + (0.5 + Math.random() * 0.5); + + defender.health -= attackerAttackDamage; + + if (defender.health <= 0) { + const msg = getRandomAttackMessage( + defender.health <= -getMaxHealth(defender) + ? attackerAttackInfo.messagesOverpower + : attackerAttackInfo.messagesDead, + ); + client.sendTextMessage(roomId, msg); + + return true; + } + + const msg = getRandomAttackMessage(attackerAttackInfo.messages); + client.sendTextMessage(roomId, msg); + + return false; +}; + +const respawnPlayer = ( + client: MatrixClient, + roomId: string, + player: IPlayer, +) => { + const defaultRespawn = locationFarlands; + + player.location = defaultRespawn.id; + player.health = getMaxHealth(player); + + client.sendTextMessage( + roomId, + `${player.name} has been respawned in ${defaultRespawn.name}`, + ); +}; + +const getRandomAttackMessage = (messages: string[]): string => { + return messages[Math.floor(Math.random() * messages.length)] ?? ""; +}; + +export { fightEntity, fightRound, respawnPlayer, getRandomAttackMessage }; diff --git a/src/services/game/structures/entities.ts b/src/services/game/structures/entities.ts index c3fdec7..afe2ef0 100644 --- a/src/services/game/structures/entities.ts +++ b/src/services/game/structures/entities.ts @@ -1,6 +1,7 @@ import { Location } from "./locations.js"; import { Race } from "./races.js"; import type { IInventory } from "../types.js"; +import { DamageType } from "./items.js"; export interface IEntity { name: string; @@ -19,9 +20,11 @@ export interface IEntity { stealth: number; charisma: number; lockpicking: number; + attacks: Attack[]; } export interface IPlayer extends IEntity { + isPlayer: true; health: number; } @@ -49,6 +52,120 @@ export enum NpcType { AGGRESIVE = "AGGRESIVE", } +export interface IAttack { + id: Attack; + damageType: DamageType; + damageMultiplier: number; + messages: string[]; + messagesDead: string[]; + messagesOverpower: string[]; +} + +export enum Attack { + PUNCH = "PUNCH", + CUT = "CUT", + STAB = "STAB", + KICK = "KICK", + STOMP = "STOMP", + SIT = "SIT", + RIP = "RIP", +} + +export const attackPunch: IAttack = { + id: Attack.PUNCH, + damageType: DamageType.PHYSICAL, + damageMultiplier: 0.5, + messages: ["ATTACKER punches DEFENDER"], + messagesDead: [ + "ATTACKER punches DEFENDER to death", + "ATTACKER beats DEFENDER to death", + ], + messagesOverpower: ["ATTACKER punches DEFENDER into mush"], +}; + +export const attackCut: IAttack = { + id: Attack.CUT, + damageType: DamageType.PHYSICAL, + damageMultiplier: 1, + messages: ["ATTACKER cuts DEFENDER"], + messagesDead: [ + "ATTACKER cuts DEFENDER in half", + "ATTACKER cuts DEFENDER's head off", + "ATTACKER cuts DEFENDER, instantly killing them", + ], + messagesOverpower: ["ATTACKER cuts DEFENDER in half"], +}; + +export const attackStab: IAttack = { + id: Attack.STAB, + damageType: DamageType.PHYSICAL, + damageMultiplier: 1.5, + messages: ["ATTACKER stabs DEFENDER"], + messagesDead: [ + "ATTACKER stabs DEFENDER to death", + "ATTACKER stabs DEFENDER, instantly killing them", + ], + messagesOverpower: ["ATTACKER stabs DEFENDER, instantly killing them"], +}; + +export const attackKick: IAttack = { + id: Attack.KICK, + damageType: DamageType.PHYSICAL, + damageMultiplier: 0.75, + messages: [ + "ATTACKER kick DEFENDER", + "ATTACKER kick DEFENDER in their face", + ], + messagesDead: ["ATTACKER kicks DEFENDER to death"], + messagesOverpower: ["ATTACKER kicks DEFENDER into mush"], +}; + +export const attackStomp: IAttack = { + id: Attack.STOMP, + damageType: DamageType.PHYSICAL, + damageMultiplier: 0.75, + messages: [ + "ATTACKER stomps DEFENDER", + "ATTACKER stomps DEFENDER in their face", + ], + messagesDead: ["ATTACKER stomps DEFENDER to death"], + messagesOverpower: [ + "ATTACKER stomps DEFENDER into mush", + "DEFENDER explodes after ATTACKER stomps them into bloody mush", + ], +}; + +export const attackSit: IAttack = { + id: Attack.SIT, + damageType: DamageType.PHYSICAL, + damageMultiplier: 0.65, + messages: ["ATTACKER sits on DEFENDER"], + messagesDead: ["ATTACKER sits on DEFENDER and crushes them to death"], + messagesOverpower: [ + "ATTACKER sits on DEFENDER and crushes them into mush and goo", + "DEFENDER explodes after ATTACKER sits on them with their full weight", + ], +}; + +export const attackRip: IAttack = { + id: Attack.RIP, + damageType: DamageType.PHYSICAL, + damageMultiplier: 0.3, + messages: ["ATTACKER tries to rip DEFENDER's limbs off"], + messagesDead: ["ATTACKER rips DEFENDER's head off"], + messagesOverpower: ["ATTACKER effortlessly rips DEFENDER's body in half"], +}; + +export const attacks = [ + attackPunch, + attackCut, + attackStab, + attackKick, + attackStomp, + attackSit, + attackRip, +]; + export const npcBecky: INPC = { id: NPC.BECKY, name: "Becky", @@ -70,6 +187,7 @@ export const npcBecky: INPC = { charisma: 5, lockpicking: 50, type: NpcType.QUEST_GIVER, + attacks: [Attack.KICK, Attack.STOMP, Attack.SIT, Attack.RIP], }; export const npcTato: INPC = { @@ -93,6 +211,7 @@ export const npcTato: INPC = { charisma: 20, lockpicking: 0, type: NpcType.QUEST_GIVER, + attacks: [Attack.PUNCH], }; export const npcs = [npcBecky, npcTato]; diff --git a/src/services/game/structures/items.ts b/src/services/game/structures/items.ts index bf00f11..72c0ba1 100644 --- a/src/services/game/structures/items.ts +++ b/src/services/game/structures/items.ts @@ -1,3 +1,4 @@ +import { Attack } from "./entities.js"; import { Rarity } from "./rarities.js"; export interface IItem { @@ -8,6 +9,7 @@ export interface IItem { type: ItemType; rarity: Rarity; stats: IStat[]; + attacks: Attack[]; } export interface IStat { @@ -56,4 +58,5 @@ export const itemShortsword: IItem = { defense: 0, }, ], + attacks: [Attack.CUT, Attack.STAB], };