Add NPC interaction system with memory and quest generation
- Introduced a new NPC system with dynamic NPCs and conversation handling. - Implemented NPC memory using SQLite to log conversations and manage relationships. - Added commands for talking to NPCs, listing available NPCs, and generating quests. - Updated database schema to support NPC conversations and relationships. - Refactored code structure to separate concerns into cogs and handlers.
This commit is contained in:
Binary file not shown.
Binary file not shown.
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
# main.py
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import threading
|
||||||
|
import itertools
|
||||||
|
from npc_memory import NPCMemory
|
||||||
|
from npc_handler import NPCHandler
|
||||||
|
from cogs.npc import NPCCog
|
||||||
|
|
||||||
|
|
||||||
|
class MyNewHelp(commands.MinimalHelpCommand):
|
||||||
|
async def send_pages(self):
|
||||||
|
destination = self.get_destination()
|
||||||
|
for page in self.paginator.pages:
|
||||||
|
emby = discord.Embed(description=page)
|
||||||
|
await destination.send(embed=emby)
|
||||||
|
|
||||||
|
|
||||||
|
class Client(commands.Bot):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
command_prefix=self.iterate_prefix("py"),
|
||||||
|
strip_after_prefix=True,
|
||||||
|
case_insensitive=True,
|
||||||
|
intents=discord.Intents.all(),
|
||||||
|
help_command=MyNewHelp(),
|
||||||
|
)
|
||||||
|
def iterate_prefix(self, prefix):
|
||||||
|
prefixes = list(map(''.join, itertools.product(*zip(prefix.upper(), prefix.lower()))))
|
||||||
|
print(prefixes)
|
||||||
|
|
||||||
|
return prefixes
|
||||||
|
|
||||||
|
async def setup_hook(self): # overwriting a handler
|
||||||
|
cogs_folder = f"{os.path.abspath(os.path.dirname(__file__))}/cogs"
|
||||||
|
for filename in os.listdir(cogs_folder):
|
||||||
|
if filename.endswith(".py"):
|
||||||
|
try:
|
||||||
|
await self.load_extension(f"cogs.{filename[:-3]}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to load {filename}: {e}")
|
||||||
|
memory = NPCMemory()
|
||||||
|
npc_handler = NPCHandler(memory)
|
||||||
|
await self.add_cog(NPCCog(self, npc_handler))
|
||||||
|
await self.tree.sync()
|
||||||
|
print("Loaded cogs")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
load_dotenv()
|
||||||
|
client = Client()
|
||||||
|
token = os.getenv("TOKEN")
|
||||||
|
if token is not None:
|
||||||
|
client.run(token)
|
||||||
|
else:
|
||||||
|
print("Token is missing.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Binary file not shown.
@@ -0,0 +1,51 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
|
||||||
|
class NPCCog(commands.Cog):
|
||||||
|
def __init__(self, bot, npc_handler):
|
||||||
|
self.bot = bot
|
||||||
|
self.npc_handler = npc_handler
|
||||||
|
self.player_contexts = {}
|
||||||
|
|
||||||
|
@commands.command(name="talk")
|
||||||
|
async def talk_to_npc(self, ctx, npc_name: str, *, message: str):
|
||||||
|
player_id = str(ctx.author.id)
|
||||||
|
if player_id not in self.player_contexts:
|
||||||
|
self.player_contexts[player_id] = {
|
||||||
|
'id': player_id,
|
||||||
|
'level': 1,
|
||||||
|
'reputation': 0,
|
||||||
|
'recent_actions': [],
|
||||||
|
'location': 'Newhaven'
|
||||||
|
}
|
||||||
|
response = self.npc_handler.chat_with_npc(
|
||||||
|
npc_name, message, self.player_contexts[player_id]
|
||||||
|
)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{npc_name} says...",
|
||||||
|
description=response,
|
||||||
|
color=discord.Color.blue()
|
||||||
|
)
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
@commands.command(name="npcs")
|
||||||
|
async def list_npcs(self, ctx):
|
||||||
|
npc_list = "\n".join([f"• {name}" for name in self.npc_handler.npcs.keys()])
|
||||||
|
await ctx.send(f"Available NPCs:\n{npc_list}")
|
||||||
|
|
||||||
|
@commands.command(name="quest")
|
||||||
|
async def get_quest(self, ctx, npc_name: str):
|
||||||
|
player_id = str(ctx.author.id)
|
||||||
|
player_level = self.player_contexts.get(player_id, {}).get('level', 1)
|
||||||
|
quest = self.npc_handler.generate_quest(npc_name, player_level)
|
||||||
|
if quest:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=quest["title"],
|
||||||
|
description=quest["description"],
|
||||||
|
color=discord.Color.gold()
|
||||||
|
)
|
||||||
|
embed.add_field(name="Reward", value=quest["reward"])
|
||||||
|
embed.add_field(name="Difficulty", value=quest["difficulty"])
|
||||||
|
await ctx.send(embed=embed)
|
||||||
|
else:
|
||||||
|
await ctx.send("NPC not found or unable to generate quest.")
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import json
|
||||||
|
import random
|
||||||
|
|
||||||
|
NPC_DATABASE = {
|
||||||
|
"barkeep_boris": {
|
||||||
|
"personality": "Jovial and gossipy, knows everyone's business",
|
||||||
|
"backstory": "Retired adventurer who settled down after losing party to dragon",
|
||||||
|
"quirks": ["Offers free drinks to good storytellers", "Hates elves"]
|
||||||
|
},
|
||||||
|
"mad_wizard": {
|
||||||
|
"personality": "Eccentric and forgetful, brilliant but scattered",
|
||||||
|
"backstory": "Expelled from wizard college for 'creative' spellcasting",
|
||||||
|
"quirks": ["Speaks to imaginary familiar", "Offers dangerous experimental potions"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DynamicNPC:
|
||||||
|
def __init__(self, name, data):
|
||||||
|
self.name = name
|
||||||
|
self.personality = data["personality"]
|
||||||
|
self.backstory = data["backstory"]
|
||||||
|
self.quirks = data["quirks"]
|
||||||
|
|
||||||
|
class NPCHandler:
|
||||||
|
def __init__(self, memory):
|
||||||
|
self.memory = memory
|
||||||
|
self.npcs = {name: DynamicNPC(name, data) for name, data in NPC_DATABASE.items()}
|
||||||
|
|
||||||
|
def chat_with_npc(self, npc_name, message, player_context):
|
||||||
|
npc = self.npcs.get(npc_name)
|
||||||
|
if not npc:
|
||||||
|
return "That NPC doesn't exist."
|
||||||
|
# Simple response logic (replace with LLM call as needed)
|
||||||
|
response = f"{npc.name} ({npc.personality}): I heard you say '{message}'."
|
||||||
|
self.memory.log_conversation(npc_name, player_context['id'], message, response)
|
||||||
|
self.memory.update_affinity(npc_name, player_context['id'], 1)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def generate_quest(self, npc_name, player_level):
|
||||||
|
npc = self.npcs.get(npc_name)
|
||||||
|
if not npc:
|
||||||
|
return None
|
||||||
|
# Replace this with LLM call if available
|
||||||
|
quest = {
|
||||||
|
"title": f"{npc.name}'s Request",
|
||||||
|
"description": f"Help {npc.name} with a task suitable for level {player_level}.",
|
||||||
|
"reward": f"{random.randint(10, 100)} coins",
|
||||||
|
"difficulty": random.choice(["easy", "medium", "hard"])
|
||||||
|
}
|
||||||
|
return quest
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
class NPCMemory:
|
||||||
|
def __init__(self):
|
||||||
|
self.conn = sqlite3.connect('npc_memory.db')
|
||||||
|
self.create_tables()
|
||||||
|
|
||||||
|
def create_tables(self):
|
||||||
|
self.conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS npc_conversations (
|
||||||
|
npc_id TEXT,
|
||||||
|
player_id TEXT,
|
||||||
|
message TEXT,
|
||||||
|
response TEXT,
|
||||||
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
self.conn.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS npc_relationships (
|
||||||
|
npc_id TEXT,
|
||||||
|
player_id TEXT,
|
||||||
|
affinity INTEGER DEFAULT 0,
|
||||||
|
last_interaction DATETIME
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def log_conversation(self, npc_id, player_id, message, response):
|
||||||
|
self.conn.execute(
|
||||||
|
'INSERT INTO npc_conversations (npc_id, player_id, message, response) VALUES (?, ?, ?, ?)',
|
||||||
|
(npc_id, player_id, message, response)
|
||||||
|
)
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def update_affinity(self, npc_id, player_id, delta):
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
cur.execute('SELECT affinity FROM npc_relationships WHERE npc_id=? AND player_id=?', (npc_id, player_id))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row:
|
||||||
|
new_affinity = row[0] + delta
|
||||||
|
cur.execute('UPDATE npc_relationships SET affinity=?, last_interaction=CURRENT_TIMESTAMP WHERE npc_id=? AND player_id=?',
|
||||||
|
(new_affinity, npc_id, player_id))
|
||||||
|
else:
|
||||||
|
cur.execute('INSERT INTO npc_relationships (npc_id, player_id, affinity, last_interaction) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
|
||||||
|
(npc_id, player_id, delta))
|
||||||
|
self.conn.commit()
|
||||||
@@ -5,10 +5,12 @@ import os
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from web.app import app
|
from web.app import app
|
||||||
import threading
|
import threading
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
|
||||||
def run_web():
|
def run_web():
|
||||||
app.run(debug=False, host="0.0.0.0", port=5000)
|
app.run(debug=False, host="0.0.0.0", port=8080)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
class MyNewHelp(commands.MinimalHelpCommand):
|
class MyNewHelp(commands.MinimalHelpCommand):
|
||||||
@@ -22,12 +24,17 @@ class MyNewHelp(commands.MinimalHelpCommand):
|
|||||||
class Client(commands.Bot):
|
class Client(commands.Bot):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
command_prefix=["pY ", "PY ", "Py ", "py "],
|
command_prefix=self.iterate_prefix("py"),
|
||||||
strip_after_prefix=True,
|
strip_after_prefix=True,
|
||||||
case_insensitive=True,
|
case_insensitive=True,
|
||||||
intents=discord.Intents.all(),
|
intents=discord.Intents.all(),
|
||||||
help_command=MyNewHelp(),
|
help_command=MyNewHelp(),
|
||||||
)
|
)
|
||||||
|
def iterate_prefix(self, prefix):
|
||||||
|
prefixes = list(map(''.join, itertools.product(*zip(prefix.upper(), prefix.lower()))))
|
||||||
|
print(prefixes)
|
||||||
|
|
||||||
|
return prefixes
|
||||||
|
|
||||||
async def setup_hook(self): # overwriting a handler
|
async def setup_hook(self): # overwriting a handler
|
||||||
cogs_folder = f"{os.path.abspath(os.path.dirname(__file__))}/cogs"
|
cogs_folder = f"{os.path.abspath(os.path.dirname(__file__))}/cogs"
|
||||||
|
|||||||
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
+2
-2
@@ -3,7 +3,7 @@ from random import shuffle, choices, choice
|
|||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from utils.sql_commands import DatabaseManager
|
from utils.sql_commands import DatabaseManager
|
||||||
from utils.bank_functions import bank_data, update_money
|
from utils.bank_functions import bank_data, update_money
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
import asyncio
|
import asyncio
|
||||||
import random
|
import random
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class Deck:
|
|||||||
|
|
||||||
|
|
||||||
class Hand:
|
class Hand:
|
||||||
def __init__(self, name, bet):
|
def __init__(self, name:str, bet:int):
|
||||||
self.cards = []
|
self.cards = []
|
||||||
self.value = 0
|
self.value = 0
|
||||||
self.aces = 0
|
self.aces = 0
|
||||||
|
|||||||
+4
-5
@@ -1,4 +1,3 @@
|
|||||||
import discord
|
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
@@ -10,7 +9,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
|
|
||||||
class Mail(commands.Cog):
|
class Mail(commands.Cog):
|
||||||
def __init__(self, client):
|
def __init__(self, client:commands.Bot):
|
||||||
self.client = client
|
self.client = client
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
from utils.sql_commands import DatabaseManager
|
from utils.sql_commands import DatabaseManager
|
||||||
@@ -19,7 +18,7 @@ class Mail(commands.Cog):
|
|||||||
|
|
||||||
@commands.is_owner()
|
@commands.is_owner()
|
||||||
@commands.command(name="mail_feedback")
|
@commands.command(name="mail_feedback")
|
||||||
async def mail(self, ctx):
|
async def mail(self, ctx:commands.Context[commands.Bot]):
|
||||||
password = getenv("EMAILPASS")
|
password = getenv("EMAILPASS")
|
||||||
username = getenv("EMAILUSER")
|
username = getenv("EMAILUSER")
|
||||||
server = getenv("EMAILSERVER")
|
server = getenv("EMAILSERVER")
|
||||||
@@ -44,7 +43,7 @@ class Mail(commands.Cog):
|
|||||||
msg["Subject"] = "Py feedback"
|
msg["Subject"] = "Py feedback"
|
||||||
|
|
||||||
# Fetch feedback from the database
|
# Fetch feedback from the database
|
||||||
feedback_rows = self.db.fetch_all("SELECT * FROM feedback")
|
feedback_rows:list[dict[str,str]] = self.db.fetch_all("SELECT * FROM feedback")
|
||||||
all_feedback = ""
|
all_feedback = ""
|
||||||
for i, row in enumerate(feedback_rows, 1):
|
for i, row in enumerate(feedback_rows, 1):
|
||||||
content = html.escape(row["CONTENT"])
|
content = html.escape(row["CONTENT"])
|
||||||
@@ -131,5 +130,5 @@ class Mail(commands.Cog):
|
|||||||
await ctx.reply(f"Failed to send mail: {e}", delete_after=5)
|
await ctx.reply(f"Failed to send mail: {e}", delete_after=5)
|
||||||
|
|
||||||
|
|
||||||
async def setup(client):
|
async def setup(client:commands.Bot):
|
||||||
await client.add_cog(Mail(client))
|
await client.add_cog(Mail(client))
|
||||||
|
|||||||
+1
-1
@@ -241,5 +241,5 @@ class XP(commands.Cog):
|
|||||||
return image_bytes
|
return image_bytes
|
||||||
|
|
||||||
|
|
||||||
async def setup(client):
|
async def setup(client: commands.Bot):
|
||||||
await client.add_cog(XP(client))
|
await client.add_cog(XP(client))
|
||||||
|
|||||||
+7199
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"messages": {
|
||||||
|
"601579326714019840": {
|
||||||
|
"total": 32,
|
||||||
|
"commands": 27,
|
||||||
|
"non_commands": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"purge": 2,
|
||||||
|
"mail_feedback": 1,
|
||||||
|
"help": 5,
|
||||||
|
"nuke": 1,
|
||||||
|
"ping": 1,
|
||||||
|
"top": 1,
|
||||||
|
"stats": 1,
|
||||||
|
"poker": 7,
|
||||||
|
"balance": 2,
|
||||||
|
"daily": 1,
|
||||||
|
"withdraw": 1,
|
||||||
|
"leaderboard": 1,
|
||||||
|
"afk": 1,
|
||||||
|
"afklist": 1,
|
||||||
|
"whois": 1
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"bot": 32
|
||||||
|
},
|
||||||
|
"guilds": {
|
||||||
|
"Plex": 32
|
||||||
|
},
|
||||||
|
"total_messages": 0,
|
||||||
|
"command_messages": 0,
|
||||||
|
"non_command_messages": 0
|
||||||
|
}
|
||||||
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
@@ -203,6 +203,8 @@ class DatabaseManager:
|
|||||||
query = f"{query} ({', '.join(keys)}) VALUES ({placeholders})"
|
query = f"{query} ({', '.join(keys)}) VALUES ({placeholders})"
|
||||||
values = [tuple(data.values()) for data in params]
|
values = [tuple(data.values()) for data in params]
|
||||||
|
|
||||||
|
connection = None
|
||||||
|
cursor = None
|
||||||
try:
|
try:
|
||||||
connection = self.get_connection()
|
connection = self.get_connection()
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
@@ -213,10 +215,13 @@ class DatabaseManager:
|
|||||||
)
|
)
|
||||||
except mysql.connector.Error as err:
|
except mysql.connector.Error as err:
|
||||||
logger.error(f"Bulk insert failed: {err}")
|
logger.error(f"Bulk insert failed: {err}")
|
||||||
connection.rollback() # Roll back on error
|
if connection:
|
||||||
|
connection.rollback() # Roll back on error
|
||||||
finally:
|
finally:
|
||||||
cursor.close()
|
if cursor:
|
||||||
connection.close()
|
cursor.close()
|
||||||
|
if connection:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
def delete(self, table_name: str, condition: dict) -> None:
|
def delete(self, table_name: str, condition: dict) -> None:
|
||||||
"""Deletes a record from the specified table based on the condition provided."""
|
"""Deletes a record from the specified table based on the condition provided."""
|
||||||
|
|||||||
Executable → Regular
BIN
Binary file not shown.
Executable → Regular
BIN
Binary file not shown.
Reference in New Issue
Block a user