From: xangelo Date: Tue, 3 Dec 2024 05:53:33 +0000 (-0500) Subject: feat(crafting): add monster drops X-Git-Tag: v0.4.4~37 X-Git-Url: https://git.xangelo.ca/?a=commitdiff_plain;h=078ab3f34462a1ece53417bff54f10932bc2b60f;p=risinglegends.git 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 --- 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 0000000..8d350e3 Binary files /dev/null and b/public/assets/img/icons/items/Light_Hide.png differ diff --git a/seeds/drop-tables.ts b/seeds/drop-tables.ts new file mode 100644 index 0000000..a12e1c8 --- /dev/null +++ b/seeds/drop-tables.ts @@ -0,0 +1,64 @@ +import { config as dotenv } from 'dotenv'; +import Airtable from 'airtable'; +import { db } from '@server/lib/db'; +import { logger } from '@server/lib/logger' + +dotenv(); + +Airtable.configure({ + apiKey: process.env.AIRTABLE_API_KEY +}); + +const base = Airtable.base('appDfPLPajPNog5Iw'); + +export async function createDropTables(): Promise { + 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; +}