From f8c11c8e20bb36bb330ef43b40c29a6b354451b2 Mon Sep 17 00:00:00 2001 From: Justin Oro Date: Fri, 11 Sep 2026 12:25:55 -0700 Subject: [PATCH] prefill mapgenie search box with the item name in !wiki map links --- README.md | 3 ++- index.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f3d7998..9c7b888 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,8 @@ To change the token or key later, edit `.env` directly, then run the installer a - The bot only replies to mentions of its trigger word if Ollama is installed, running, and has the model in `OLLAMA_MODEL` downloaded - without that, mentioning it gets no reply at all, and nothing is logged (check `bot.error.log` if you expect it to be working and it isn't replying). Each person can only trigger an AI answer once every 5 seconds; asking again sooner (or the Ollama request failing) just gets no reply rather than a second request, which keeps a burst of questions from bogging down your computer generating several replies at once. - `!remember SOMETHING` teaches it a permanent fact - a name, a preference, a correction like "don't call me buddy", or something about another member mentioned by name - that it will always take into account, even after the bot restarts. These facts are shared: anyone can ask about a fact someone else taught it, not just the person who typed `!remember`. They're saved to `memory.yaml` under whoever typed the command, and never expire on their own, unlike the short-term memory described below. `!forget SOMETHING` removes one you personally taught it (it has to match what you typed to `!remember`, word for word - use `!memories` to see the exact wording it has stored under your name), and `!forget all` clears everything you've had it remember. Each person can have up to 20 things remembered at a time. - `OLLAMA_MODEL` in `.env` controls which model it uses (`llama3.2` by default - small, fast, and good enough for short in-character replies). To use a different one, pull it yourself with `ollama pull MODEL_NAME`, set `OLLAMA_MODEL=MODEL_NAME` in `.env`, and restart it (re-run the installer, or `pm2 restart discord-bot`). Bigger models give better answers but take longer to reply and use more of your computer's memory. -- `TAVILY_API_KEY` in `.env` is optional and off by default, and is what `!wiki QUESTION` needs to work. Without it, `!wiki` just says AI isn't set up for that. With it set, `!wiki` runs a real web search and answers with what it finds, which for specific game facts (an exact item location, a boss's weak point) is far more likely to be accurate than the small local model guessing on its own - unlike the AI chat itself, this does send your question to Tavily's search API over the internet. Get a free key (no credit card required, 1,000 searches a month free) at [app.tavily.com](https://app.tavily.com/) (the installer asks for this too and lets you skip it), then restart the bot (re-run the installer, or `pm2 restart discord-bot`) to pick it up. Plain mentions of the trigger word never use this - they always stay pure in-character chat from the local model, so `!wiki` is the one to reach for when you want a real answer instead of banter. If the search results for a `!wiki` answer happen to include a page from [mapgenie.io](https://mapgenie.io) (interactive maps for a lot of survival/open-world games), it adds that link to the reply too - there's no per-game setup for this, it just checks whatever the search turns up. +- `TAVILY_API_KEY` in `.env` is optional and off by default, and is what `!wiki QUESTION` needs to work. Without it, `!wiki` just says AI isn't set up for that. With it set, `!wiki` runs a real web search and answers with what it finds, which for specific game facts (an exact item location, a boss's weak point) is far more likely to be accurate than the small local model guessing on its own - unlike the AI chat itself, this does send your question to Tavily's search API over the internet. Get a free key (no credit card required, 1,000 searches a month free) at [app.tavily.com](https://app.tavily.com/) (the installer asks for this too and lets you skip it), then restart the bot (re-run the installer, or `pm2 restart discord-bot`) to pick it up. Plain mentions of the trigger word never use this - they always stay pure in-character chat from the local model, so `!wiki` is the one to reach for when you want a real answer instead of banter. +- `!wiki` also links an interactive map from [mapgenie.io](https://mapgenie.io) when it has one for the game being asked about, using `maps.json` in this folder - a plain list of `"game name": "mapgenie URL"` pairs. It ships with one entry (Sons Of The Forest) as an example; add more yourself as you find them by browsing to the game on mapgenie.io and copying its URL. This is a direct lookup, not a live search, because mapgenie's map pages are built with client-side JavaScript that search engines mostly can't read the content of, so relying on search alone to find them essentially never works even when a page exists. Edit `maps.json` and restart the bot (re-run the installer, or `pm2 restart discord-bot`) to pick up changes. - It remembers each person's last 3 exchanges (their message and its reply) so it can follow up naturally and stick to things you told it earlier in the conversation, like a correction or a preference. This memory is per-person, kept only in the bot's running memory (not saved to disk), and forgotten automatically after 30 minutes of that person not mentioning it. - The bot's name, trigger word, and personality all come from `personality.yaml`. It ships set up as BMO from Adventure Time: ``` diff --git a/index.js b/index.js index 190c61e..3f91d32 100644 --- a/index.js +++ b/index.js @@ -35,6 +35,7 @@ const OWNED_PATH = path.join(__dirname, "owned.json"); const STEAMLINKS_PATH = path.join(__dirname, "steamlinks.json"); const SCORES_PATH = path.join(__dirname, "scores.json"); const TRIVIA_PATH = path.join(__dirname, "trivia.json"); +const MAPS_PATH = path.join(__dirname, "maps.json"); const GREETINGS_PATH = path.join(__dirname, "greetings.yaml"); const GOODBYES_PATH = path.join(__dirname, "goodbyes.yaml"); const README_PATH = path.join(__dirname, "README.md"); @@ -90,6 +91,48 @@ function findMapLink(results) { return hit ? hit.url : null; } +function loadGameMaps() { + try { + const parsed = JSON.parse(fs.readFileSync(MAPS_PATH, "utf8")); + return (parsed && typeof parsed === "object") ? parsed : {}; + } catch (err) { + return {}; + } +} + +const GAME_MAPS = loadGameMaps(); + +function findKnownGameMap(query) { + const normalized = normalizeTitle(query); + for (const name of Object.keys(GAME_MAPS)) { + if (normalized.includes(name)) return { name: name, url: GAME_MAPS[name] }; + } + return null; +} + +const MAP_SEARCH_FILLER_PHRASES = [ + /\bwhere\s+(can\s+i\s+find|do\s+i\s+find|is|are|to\s+find)\b/g, + /\bhow\s+(do\s+i|to)\s+(get|find)\b/g, + /\blocation\s+of\b/g +]; +const MAP_SEARCH_STOPWORDS = /\b(the|a|an|on|in|for|of|to|at|near|can|i|do|does|find|get)\b/g; + +function extractMapSearchTerm(query, gameName) { + let text = normalizeTitle(query); + if (gameName) text = text.split(gameName).join(" "); + MAP_SEARCH_FILLER_PHRASES.forEach((pattern) => { + text = text.replace(pattern, " "); + }); + text = text.replace(MAP_SEARCH_STOPWORDS, " ").replace(/\s+/g, " ").trim(); + return text; +} + +function appendMapSearch(url, term) { + if (!term) return url; + const separator = url.includes("?") ? "&" : "?"; + return url + separator + "search=" + encodeURIComponent(term); +} + async function searchWeb(query) { if (!WEB_SEARCH_ENABLED || !query) return { answer: null, results: [] }; const controller = new AbortController(); @@ -1630,7 +1673,9 @@ async function handleWiki(message, query) { ? capToSentences(searchResults.answer.trim(), AI_MAX_SENTENCES) : await askAI(query, history, displayName, allFacts, searchResults); if (aiReply) { - const mapLink = findMapLink(searchResults && searchResults.results); + const knownMap = findKnownGameMap(query); + const mapLink = findMapLink(searchResults && searchResults.results) || + (knownMap ? appendMapSearch(knownMap.url, extractMapSearchTerm(query, knownMap.name)) : null); const content = aiReply + (mapLink ? "\nInteractive map: " + mapLink : ""); await message.reply(content); rememberAiExchange(message.author.id, query, aiReply);