diff --git a/src/modules/game/game.ts b/src/modules/game/game.ts
index 2a10536..0c5ea6b 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) => {
@@ -97,7 +102,7 @@ const onHelp = (_text: string, roomId: string) => {
!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
+ !game fight {entity} - Fight an entity
(WIP) !game work {action} - Start work
`,
);
@@ -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],
};