From 078ab3f34462a1ece53417bff54f10932bc2b60f Mon Sep 17 00:00:00 2001 From: xangelo Date: Tue, 3 Dec 2024 00:53:33 -0500 Subject: [PATCH] feat(crafting): add monster drops We now have per-monster drop tables that link back to items (Airtable). It allows us to select an item + monster, and set a drop rate for that item, as well as a min/max quantity. When the monster is killed, we use the drop table to determine how many of that item to drop. That item gets added to the players Items inventory. In order to support the fact that crafting isn't in play yet, if an item doesn't have an effect_name, we disable the "use" button. Eventually these crafting items will be only usable through the crafting system to improve your existing weapons/armour --- migrations/20241202190606_drop_tables.ts | 19 ++++++ public/assets/css/inventory.css | 5 ++ public/assets/css/tabs.css | 2 +- public/assets/img/icons/items/Light_Hide.png | Bin 0 -> 3163 bytes seeds/drop-tables.ts | 64 ++++++++++++++++++ src/server/chat-commands.ts | 2 + src/server/chat-commands/index.ts | 2 + .../chat-commands/refresh-droptables.ts | 16 +++++ src/server/fight.ts | 11 +++ src/server/items.ts | 6 ++ src/server/monster.ts | 33 ++++++++- src/server/routes/chat.ts | 4 +- src/server/routes/inventory.ts | 2 +- src/server/views/fight.ts | 4 ++ src/shared/fight.ts | 4 +- src/shared/items/index.ts | 5 ++ src/shared/monsters.ts | 8 +++ 17 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 migrations/20241202190606_drop_tables.ts create mode 100644 public/assets/img/icons/items/Light_Hide.png create mode 100644 seeds/drop-tables.ts create mode 100644 src/server/chat-commands/refresh-droptables.ts diff --git a/migrations/20241202190606_drop_tables.ts b/migrations/20241202190606_drop_tables.ts new file mode 100644 index 0000000..bc16516 --- /dev/null +++ b/migrations/20241202190606_drop_tables.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + + +export async function up(knex: Knex): Promise { + return knex.schema.createTable("item_drop_tables", t => { + t.integer('item_id').references('id').inTable('items'); + t.integer('monster_id').references('id').inTable('monsters'); + t.double('drop_rate'); + t.integer('min_amount'); + t.integer('max_amount'); + t.primary(['item_id', 'monster_id']); + }); +} + + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable("item_drop_tables"); +} + diff --git a/public/assets/css/inventory.css b/public/assets/css/inventory.css index 50d348f..e59a3ff 100644 --- a/public/assets/css/inventory.css +++ b/public/assets/css/inventory.css @@ -54,6 +54,7 @@ img { filter: grayscale(40%); + width: 100%; &:hover { filter: none; @@ -128,7 +129,11 @@ transition: opacity 0.2s; z-index: 100; } +} +#filter_ITEMS { + display: flex; + flex-wrap: wrap; } .icon-durability-container { diff --git a/public/assets/css/tabs.css b/public/assets/css/tabs.css index 7a7ba44..0cc38bf 100644 --- a/public/assets/css/tabs.css +++ b/public/assets/css/tabs.css @@ -63,7 +63,7 @@ nav { display: none; &.active { - display: block !important; + display: block } } } diff --git a/public/assets/img/icons/items/Light_Hide.png b/public/assets/img/icons/items/Light_Hide.png new file mode 100644 index 0000000000000000000000000000000000000000..8d350e3bfeb4b3eef9cc5aa66b0220bdb217ed1e GIT binary patch literal 3163 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 { + return new Promise(async (resolve, reject) => { + base('Drops').select().eachPage(async (records, next) => { + records.map(async record => { + logger.debug(record); + + const monsterId = record.fields.id_from_monster[0]; + const itemId = record.fields.id_from_item[0]; + + const [monsterExists, itemExists] = await Promise.all([ + db('monsters').where('id', monsterId).first(), + db('items').where('id', itemId).first() + ]); + + if (!monsterExists || !itemExists) { + logger.warn(`Skip invalid drop: Monster ${monsterId} or Item ${itemId} not found`); + return; + } + + // Insert/Update drop table + await db('item_drop_tables') + .insert({ + monster_id: monsterId, + item_id: itemId, + drop_rate: Number(record.fields.drop_rate), + min_amount: Number(record.fields.min_amount) || 1, + max_amount: Number(record.fields.max_amount) || 1 + }) + .onConflict(['monster_id', 'item_id']) + .merge(['drop_rate', 'min_amount', 'max_amount']); + + }); + next(); + }).finally(() => { + resolve(); + }); + }); +} + +// run this script manually +if(!module.parent) { + createDropTables() + .then(() => { + logger.info('Drop tables updated'); + process.exit(0); + }) + .catch(e => { + logger.error(e); + process.exit(1); + }); +} diff --git a/src/server/chat-commands.ts b/src/server/chat-commands.ts index da43c1c..6d428a0 100644 --- a/src/server/chat-commands.ts +++ b/src/server/chat-commands.ts @@ -3,11 +3,13 @@ import { Player } from '@shared/player'; import { broadcastMessage } from '@shared/message'; import { renderChatMessage } from './views/chat'; import { Commands } from './chat-commands/'; +import { logger } from './lib/logger'; export async function handleChatCommands(msg: string, player: Player, io: Server, sender: Socket): Promise { const rawCommand = msg.split('/server ')[1]; + logger.debug(`${player.username} running command: [${rawCommand}]`); let matched = false; Commands.forEach(async command => { if(command.regex.test(rawCommand)) { diff --git a/src/server/chat-commands/index.ts b/src/server/chat-commands/index.ts index 2ea13bc..d8f638b 100644 --- a/src/server/chat-commands/index.ts +++ b/src/server/chat-commands/index.ts @@ -1,4 +1,5 @@ import { ChatCommand } from "./base"; +import { refreshDropTables } from './refresh-droptables'; import { refreshMonsters } from "./refresh-monsters"; import { refreshCities } from './refresh-cities'; import { refreshShops } from './refresh-shops'; @@ -11,6 +12,7 @@ export const Commands = new Set(); Commands.add(refreshMonsters); Commands.add(refreshCities); Commands.add(refreshShops); +Commands.add(refreshDropTables); Commands.add(setLevel); Commands.add(say); Commands.add(setPermission); diff --git a/src/server/chat-commands/refresh-droptables.ts b/src/server/chat-commands/refresh-droptables.ts new file mode 100644 index 0000000..9b10bc2 --- /dev/null +++ b/src/server/chat-commands/refresh-droptables.ts @@ -0,0 +1,16 @@ +import { ChatCommand } from './base'; +import { Socket } from 'socket.io'; +import { createDropTables } from '../../../seeds/drop-tables'; +import { broadcastMessage } from '../../shared/message'; +import { renderChatMessage } from '../views/chat'; +import { Player } from '../../shared/player'; + +async function handler(rawCommand: string, sender: Socket, player: Player) { + if(player.permissions.includes('admin')) { + await createDropTables(); + const message = broadcastMessage('server', 'Drop tables refreshed'); + sender.emit('chat', renderChatMessage(message)); + } +} + +export const refreshDropTables = new ChatCommand('refresh-droptables', new RegExp(/^refresh-droptables/), handler); diff --git a/src/server/fight.ts b/src/server/fight.ts index 34425f6..e27a87d 100644 --- a/src/server/fight.ts +++ b/src/server/fight.ts @@ -5,6 +5,7 @@ import { getMonsterList, saveFightState, loadMonsterFromFight, + getMonsterDrop, } from "./monster"; import { Player, @@ -32,6 +33,8 @@ import { Request, Response } from "express"; import * as Alert from "./views/alert"; import { addEvent } from "./events"; import { Professions } from "../shared/profession"; +import { getShopItem } from "./shopEquipment"; +import { getItem, givePlayerItem } from "./items"; export async function blockPlayerInFight( req: Request, @@ -115,6 +118,7 @@ export async function fightRound( exp: 0, gold: 0, levelIncrease: false, + items: [] }, }; const equippedItems = await getEquippedItems(player.id); @@ -261,6 +265,13 @@ export async function fightRound( fightTrigger: roundData.monster.fight_trigger, }); + // lets check out drop rates for this monster! + const drop = await getMonsterDrop(monster.ref_id); + if (drop) { + await givePlayerItem(player.id, drop.id, drop.amount); + roundData.rewards.items.push(drop); + } + const expGained = exponentialExp(monster.exp, monster.level, player.level); roundData.rewards.exp = expGained; diff --git a/src/server/items.ts b/src/server/items.ts index fc39bbc..1eac0be 100644 --- a/src/server/items.ts +++ b/src/server/items.ts @@ -48,3 +48,9 @@ export async function updateItemCount(player_id: string, item_id: number, delta: return rows[0].amount; } + +export async function getItem(item_id: number): Promise { + return db.select('*').from('items').where({ + id: item_id + }).first(); +} diff --git a/src/server/monster.ts b/src/server/monster.ts index c0e6651..3e7e813 100644 --- a/src/server/monster.ts +++ b/src/server/monster.ts @@ -4,7 +4,9 @@ import { TimePeriod, TimeManager } from '@shared/time'; import { LocationWithCity } from '@shared/map'; import { random, sample } from 'lodash'; import { CHANCE_TO_FIGHT_SPECIAL } from '@shared/constants'; - +import { ItemDropTable } from '@shared/monsters'; +import { DroppedItem } from '@shared/items'; +import { getItem } from './items'; const time = new TimeManager(); /** @@ -135,3 +137,32 @@ export async function clearFight(authToken: string) { player_id: authToken }).delete(); } + +export async function getDropTable(monsterId: number): Promise { + return db('item_drop_tables').where('monster_id', monsterId); +} + +export async function getMonsterDrop(monsterId: number): Promise { + const drops = await getDropTable(monsterId); + const roll = Math.random(); + + let currentWeight = 0; + let droppedItem: ItemDropTable | null = null; + for (const drop of drops) { + currentWeight += drop.drop_rate; + if (roll <= currentWeight) { + droppedItem = drop; + } + } + + if(droppedItem) { + const amount = random(droppedItem.min_amount, droppedItem.max_amount); + return { + ...(await getItem(droppedItem.item_id)), + monster_id: droppedItem.monster_id, + amount, + }; + } + + return null; +} diff --git a/src/server/routes/chat.ts b/src/server/routes/chat.ts index cc41b80..ff11e89 100644 --- a/src/server/routes/chat.ts +++ b/src/server/routes/chat.ts @@ -4,7 +4,7 @@ import { authEndpoint } from '../auth'; import { renderChatMessage } from '../views/chat'; import { handleChatCommands } from '../chat-commands'; import xss from 'xss'; - +import { logger } from '../lib/logger'; export const chatRouter = Router(); const chatHistory: Message[] = []; @@ -23,6 +23,8 @@ chatRouter.post('/chat', authEndpoint, async (req: Request, res: Response) => { return; } + logger.debug(`${req.player.username} sending message: [${msg}] (${req.player.permissions.join(', ')})`); + if(msg.startsWith('/server') && req.player.permissions.includes('admin')) { const sender = req.rl.io.sockets.sockets.get(req.rl.cache.get(`socket:${req.player.id}`)); try { diff --git a/src/server/routes/inventory.ts b/src/server/routes/inventory.ts index 841dbe1..8af4557 100644 --- a/src/server/routes/inventory.ts +++ b/src/server/routes/inventory.ts @@ -155,7 +155,7 @@ inventoryRouter.get('/modal/items/:item_id', authEndpoint, async (req: Request,
- + ${item.effect_name ? `` : ''}
diff --git a/src/server/views/fight.ts b/src/server/views/fight.ts index b773fba..9981ff7 100644 --- a/src/server/views/fight.ts +++ b/src/server/views/fight.ts @@ -21,6 +21,10 @@ export function renderRoundDetails(roundData: FightRound): string { if(roundData.rewards.levelIncrease) { html.push(`
You gained a level! ${roundData.player.level}
`); } + + roundData.rewards.items.forEach(item => { + html.push(`
${roundData.monster.name} dropped ${item.amount} ${item.name}
`); + }); break; case 'monster': if(roundData.player.hp === 0) { diff --git a/src/shared/fight.ts b/src/shared/fight.ts index 22f0ed9..63c0715 100644 --- a/src/shared/fight.ts +++ b/src/shared/fight.ts @@ -1,10 +1,12 @@ +import { DroppedItem, Item, ShopItem } from "./items" import {Fight, FightTrigger, MonsterWithFaction} from "./monsters" import {Player} from "./player" export type FightReward = { exp: number, gold: number, - levelIncrease: boolean + levelIncrease: boolean, + items: DroppedItem[] } export type FightRound = { diff --git a/src/shared/items/index.ts b/src/shared/items/index.ts index eeb0aef..5a135f6 100644 --- a/src/shared/items/index.ts +++ b/src/shared/items/index.ts @@ -9,6 +9,11 @@ export type Item = { icon_name: string; } +export type DroppedItem = Item & { + monster_id: number; + amount: number; +} + // this is the result of a join, the actual // table only maps player_id/item_id + amount export type PlayerItem = { diff --git a/src/shared/monsters.ts b/src/shared/monsters.ts index 854eee5..203d6e2 100644 --- a/src/shared/monsters.ts +++ b/src/shared/monsters.ts @@ -102,3 +102,11 @@ export const MonsterVariants: MonsterVariant[] = [ defence: 0.8 } ]; + +export type ItemDropTable = { + monster_id: number; + item_id: number; + drop_rate: number; + min_amount: number; + max_amount: number; +} -- 2.25.1