prefill mapgenie search box with the item name in !wiki map links

This commit is contained in:
Justin Oro
2026-09-11 12:25:55 -07:00
parent ab71bd7da9
commit f8c11c8e20
2 changed files with 48 additions and 2 deletions
+2 -1
View File
@@ -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:
```
+46 -1
View File
@@ -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);