Implement NPC memory and interaction system with SQLite database; add NPC data structure and dynamic NPC handling; integrate LLM for NPC conversations and quest generation.
This commit is contained in:
Binary file not shown.
Binary file not shown.
+1
-4
@@ -41,9 +41,6 @@ class Client(commands.Bot):
|
|||||||
await self.load_extension(f"cogs.{filename[:-3]}")
|
await self.load_extension(f"cogs.{filename[:-3]}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to load {filename}: {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()
|
await self.tree.sync()
|
||||||
print("Loaded cogs")
|
print("Loaded cogs")
|
||||||
|
|
||||||
@@ -51,7 +48,7 @@ class Client(commands.Bot):
|
|||||||
def main():
|
def main():
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
client = Client()
|
client = Client()
|
||||||
token = os.getenv("TOKEN")
|
token = "ODA2Mjg0OTY2NzQ0NTU1NjAw.GFQoZn.Jh0OJ7KczDOfRxFFESnAPOiodUAkjSyjQ-ClGg"
|
||||||
if token is not None:
|
if token is not None:
|
||||||
client.run(token)
|
client.run(token)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Binary file not shown.
@@ -1,50 +0,0 @@
|
|||||||
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,55 @@
|
|||||||
|
import requests
|
||||||
|
import time
|
||||||
|
|
||||||
|
# ---- CONFIG ----
|
||||||
|
OLLAMA_HOST = "http://100.103.117.14:11434" # Tailscale IP
|
||||||
|
MODEL_NAME = "neural-chat"
|
||||||
|
NUM_EXCHANGES = 10 # number of exchanges
|
||||||
|
|
||||||
|
# Names / personalities
|
||||||
|
AI1_NAME = "Alice"
|
||||||
|
AI2_NAME = "Bob"
|
||||||
|
AI1_PERSONALITY = "Alice is cheerful, curious, and friendly."
|
||||||
|
AI2_PERSONALITY = "Bob is witty, thoughtful, and calm."
|
||||||
|
|
||||||
|
# ---- HELPER FUNCTION ----
|
||||||
|
def generate_response(prompt, model="neural-chat", url="http://100.103.117.14:11434/api/generate"):
|
||||||
|
# Add instruction for short answers
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload, timeout=120)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
return data.get("response", "").strip()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"LLM error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ---- INITIAL PROMPT ----
|
||||||
|
conversation_history = f"{AI1_NAME}: Hello, my name is {AI1_NAME}. {AI1_PERSONALITY}\n" \
|
||||||
|
f"{AI2_NAME}: Hi, I'm {AI2_NAME}. {AI2_PERSONALITY}\n"
|
||||||
|
|
||||||
|
print("=== Conversation Start ===")
|
||||||
|
for i in range(NUM_EXCHANGES):
|
||||||
|
# AI1 speaks
|
||||||
|
ai1_prompt = conversation_history + f"{AI1_NAME}:"
|
||||||
|
ai1_msg = generate_response(ai1_prompt)
|
||||||
|
ai1_msg = ai1_msg.strip()
|
||||||
|
print(f"{AI1_NAME}: {ai1_msg}")
|
||||||
|
conversation_history += f"{AI1_NAME}: {ai1_msg}\n"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# AI2 speaks
|
||||||
|
ai2_prompt = conversation_history + f"{AI2_NAME}:"
|
||||||
|
ai2_msg = generate_response(ai2_prompt)
|
||||||
|
ai2_msg = ai2_msg.strip()
|
||||||
|
print(f"{AI2_NAME}: {ai2_msg}")
|
||||||
|
conversation_history += f"{AI2_NAME}: {ai2_msg}\n"
|
||||||
|
|
||||||
|
|
||||||
|
print("=== Conversation End ===")
|
||||||
@@ -24,7 +24,7 @@ class MyNewHelp(commands.MinimalHelpCommand):
|
|||||||
class Client(commands.Bot):
|
class Client(commands.Bot):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
command_prefix=self.iterate_prefix("py"),
|
command_prefix=self.iterate_prefix("Vamoc")+self.iterate_prefix("!V"),
|
||||||
strip_after_prefix=True,
|
strip_after_prefix=True,
|
||||||
case_insensitive=True,
|
case_insensitive=True,
|
||||||
intents=discord.Intents.all(),
|
intents=discord.Intents.all(),
|
||||||
@@ -52,6 +52,7 @@ def main():
|
|||||||
load_dotenv()
|
load_dotenv()
|
||||||
client = Client()
|
client = Client()
|
||||||
token = os.getenv("TOKEN")
|
token = os.getenv("TOKEN")
|
||||||
|
print(token)
|
||||||
if token is not None:
|
if token is not None:
|
||||||
threading.Thread(target=run_web, daemon=True).start()
|
threading.Thread(target=run_web, daemon=True).start()
|
||||||
client.run(token)
|
client.run(token)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,12 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from utils.npc_memory import NPCMemory
|
||||||
|
from utils.npc_handler import NPCHandler
|
||||||
|
|
||||||
class NPCCog(commands.Cog):
|
class NPCCog(commands.Cog):
|
||||||
def __init__(self, bot, npc_handler):
|
def __init__(self, bot):
|
||||||
|
npc_handler = NPCHandler(NPCMemory())
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.npc_handler = npc_handler
|
self.npc_handler = npc_handler
|
||||||
self.player_contexts = {}
|
self.player_contexts = {}
|
||||||
@@ -18,15 +22,19 @@ class NPCCog(commands.Cog):
|
|||||||
'recent_actions': [],
|
'recent_actions': [],
|
||||||
'location': 'Newhaven'
|
'location': 'Newhaven'
|
||||||
}
|
}
|
||||||
|
# Send placeholder message
|
||||||
|
thinking_msg = await ctx.send(f"**{npc_name.replace('_', ' ').title()} is thinking...**")
|
||||||
|
# Get LLM response
|
||||||
response = self.npc_handler.chat_with_npc(
|
response = self.npc_handler.chat_with_npc(
|
||||||
npc_name, message, self.player_contexts[player_id]
|
npc_name, message, self.player_contexts[player_id]
|
||||||
)
|
)
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"{npc_name} says...",
|
title=f"{npc_name.replace('_', ' ').title()} says...",
|
||||||
description=response,
|
description=response,
|
||||||
color=discord.Color.blue()
|
color=discord.Color.blue()
|
||||||
)
|
)
|
||||||
await ctx.send(embed=embed)
|
# Edit the original message with the response
|
||||||
|
await thinking_msg.edit(content=None, embed=embed)
|
||||||
|
|
||||||
@commands.command(name="npcs")
|
@commands.command(name="npcs")
|
||||||
async def list_npcs(self, ctx):
|
async def list_npcs(self, ctx):
|
||||||
@@ -48,4 +56,7 @@ class NPCCog(commands.Cog):
|
|||||||
embed.add_field(name="Difficulty", value=quest["difficulty"])
|
embed.add_field(name="Difficulty", value=quest["difficulty"])
|
||||||
await ctx.send(embed=embed)
|
await ctx.send(embed=embed)
|
||||||
else:
|
else:
|
||||||
await ctx.send("NPC not found or unable to generate quest.")
|
await ctx.send("NPC not found or unable to generate quest.")
|
||||||
|
|
||||||
|
async def setup(client):
|
||||||
|
await client.add_cog(NPCCog(client))
|
||||||
Executable → Regular
+12368
-37714
File diff suppressed because it is too large
Load Diff
+25
-15
@@ -1,33 +1,43 @@
|
|||||||
{
|
{
|
||||||
"messages": {
|
"messages": {
|
||||||
"601579326714019840": {
|
"601579326714019840": {
|
||||||
"total": 32,
|
"total": 89,
|
||||||
"commands": 27,
|
"commands": 81,
|
||||||
"non_commands": 5
|
"non_commands": 8
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
"purge": 2,
|
"purge": 5,
|
||||||
"mail_feedback": 1,
|
"mail_feedback": 1,
|
||||||
"help": 5,
|
"help": 10,
|
||||||
"nuke": 1,
|
"nuke": 2,
|
||||||
"ping": 1,
|
"ping": 1,
|
||||||
"top": 1,
|
"top": 2,
|
||||||
"stats": 1,
|
"stats": 2,
|
||||||
"poker": 7,
|
"poker": 7,
|
||||||
"balance": 2,
|
"balance": 4,
|
||||||
"daily": 1,
|
"daily": 3,
|
||||||
"withdraw": 1,
|
"withdraw": 2,
|
||||||
"leaderboard": 1,
|
"leaderboard": 2,
|
||||||
"afk": 1,
|
"afk": 1,
|
||||||
"afklist": 1,
|
"afklist": 1,
|
||||||
"whois": 1
|
"whois": 1,
|
||||||
|
"reset_money": 1,
|
||||||
|
"listcommands": 1,
|
||||||
|
"talk": 27,
|
||||||
|
"npcs": 2,
|
||||||
|
"remove_money": 1,
|
||||||
|
"guildids": 1,
|
||||||
|
"give_money": 1,
|
||||||
|
"coinflip": 2,
|
||||||
|
"deposit": 1
|
||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
"bot": 32
|
"bot": 86,
|
||||||
|
"membercount": 3
|
||||||
},
|
},
|
||||||
"guilds": {
|
"guilds": {
|
||||||
"Plex": 32
|
"Plex": 89
|
||||||
},
|
},
|
||||||
"total_messages": 0,
|
"total_messages": 0,
|
||||||
"command_messages": 0,
|
"command_messages": 0,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,74 @@
|
|||||||
|
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"],
|
||||||
|
"relationships": {
|
||||||
|
"mad_wizard": "Finds his stories amusing, sometimes annoyed by his antics",
|
||||||
|
"blacksmith_greta": "Old friends, often shares rumors",
|
||||||
|
"mysterious_stranger": "Suspicious, keeps an eye on them",
|
||||||
|
"village_healer": "Respects her, sometimes flirts",
|
||||||
|
"young_thief": "Lets him steal food, pretends not to notice"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"],
|
||||||
|
"relationships": {
|
||||||
|
"barkeep_boris": "Enjoys his company, shares magical gossip",
|
||||||
|
"blacksmith_greta": "Wants her to forge magical items, she refuses",
|
||||||
|
"mysterious_stranger": "Curious, tries to uncover their secrets",
|
||||||
|
"village_healer": "Respects her knowledge of herbs",
|
||||||
|
"young_thief": "Occasionally hires for odd errands"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blacksmith_greta": {
|
||||||
|
"personality": "Gruff but fair, takes pride in her work",
|
||||||
|
"backstory": "Inherited the forge from her father, dreams of crafting legendary weapons",
|
||||||
|
"quirks": ["Talks to her hammer", "Never removes her apron"],
|
||||||
|
"relationships": {
|
||||||
|
"barkeep_boris": "Drinks together after work, trusts him",
|
||||||
|
"mad_wizard": "Annoyed by his requests, but intrigued",
|
||||||
|
"mysterious_stranger": "Doesn't trust them, keeps her distance",
|
||||||
|
"village_healer": "Respects her, sometimes repairs her tools",
|
||||||
|
"young_thief": "Chased him out of her shop more than once"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mysterious_stranger": {
|
||||||
|
"personality": "Cryptic, speaks in riddles, always watching",
|
||||||
|
"backstory": "No one knows where they came from or what they want",
|
||||||
|
"quirks": ["Disappears when you look away", "Knows everyone's secrets"],
|
||||||
|
"relationships": {
|
||||||
|
"barkeep_boris": "Knows he is watching, uses him for information",
|
||||||
|
"mad_wizard": "Finds him amusing, but unpredictable",
|
||||||
|
"blacksmith_greta": "Avoids her, she asks too many questions",
|
||||||
|
"village_healer": "Respects her kindness, sometimes leaves gifts",
|
||||||
|
"young_thief": "Keeps an eye on him, sees potential"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"village_healer": {
|
||||||
|
"personality": "Kind, patient, and wise beyond her years",
|
||||||
|
"backstory": "Learned the healing arts from a traveling monk",
|
||||||
|
"quirks": ["Collects rare herbs", "Refuses payment for healing"],
|
||||||
|
"relationships": {
|
||||||
|
"barkeep_boris": "Enjoys his stories, sometimes worries about his health",
|
||||||
|
"mad_wizard": "Helps him with potions, tries to keep him out of trouble",
|
||||||
|
"blacksmith_greta": "Good friends, shares herbal remedies",
|
||||||
|
"mysterious_stranger": "Curious, senses a hidden pain",
|
||||||
|
"young_thief": "Treats his wounds, tries to guide him"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"young_thief": {
|
||||||
|
"personality": "Cheeky, quick-witted, always looking for trouble",
|
||||||
|
"backstory": "Grew up on the streets, steals to survive",
|
||||||
|
"quirks": ["Has a pet mouse", "Always hungry"],
|
||||||
|
"relationships": {
|
||||||
|
"barkeep_boris": "Grateful for his kindness, sometimes helps out",
|
||||||
|
"mad_wizard": "Finds him weird, but likes his tricks",
|
||||||
|
"blacksmith_greta": "Afraid of her, but admires her strength",
|
||||||
|
"mysterious_stranger": "Wants to impress, but is wary",
|
||||||
|
"village_healer": "Trusts her, sees her as a mother figure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import json
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
from .npc_data import NPC_DATABASE # <-- Add this import
|
||||||
|
|
||||||
|
def query_llm(prompt, model="neural-chat", url="http://100.103.117.14:11434/api/generate"):
|
||||||
|
# Add instruction for short answers
|
||||||
|
short_prompt = prompt + "\n\nKeep your answer short and concise (1-2 sentences)."
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"prompt": short_prompt,
|
||||||
|
"stream": False
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=payload, timeout=120)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
return data.get("response", "").strip()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"LLM error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
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."
|
||||||
|
|
||||||
|
player_id = player_context['id']
|
||||||
|
|
||||||
|
# Fetch recent conversation history (last 3 exchanges)
|
||||||
|
history = self.memory.get_conversation(npc_name, player_id)
|
||||||
|
history_str = ""
|
||||||
|
if history:
|
||||||
|
history_str = "\nRecent conversation:\n"
|
||||||
|
for player_msg, npc_reply in history[-3:]:
|
||||||
|
history_str += f"Player: {player_msg}\n{npc.name}: {npc_reply}\n"
|
||||||
|
|
||||||
|
# Fetch affinity
|
||||||
|
affinity = self.memory.get_affinity(npc_name, player_id)
|
||||||
|
affinity_str = f"Affinity with player: {affinity}\n"
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
f"You are {npc.name}, an NPC in a fantasy world.\n"
|
||||||
|
f"Personality: {npc.personality}\n"
|
||||||
|
f"Backstory: {npc.backstory}\n"
|
||||||
|
f"Quirks: {', '.join(npc.quirks)}\n"
|
||||||
|
f"{history_str}"
|
||||||
|
f"{affinity_str}"
|
||||||
|
f"Player (level {player_context.get('level', 1)}): {message}\n"
|
||||||
|
f"Respond in character as {npc.name}."
|
||||||
|
)
|
||||||
|
llm_response = query_llm(prompt)
|
||||||
|
if not llm_response:
|
||||||
|
llm_response = f"{npc.name} seems lost in thought and doesn't reply."
|
||||||
|
self.memory.log_conversation(npc_name, player_id, message, llm_response)
|
||||||
|
self.memory.update_affinity(npc_name, player_id, 1)
|
||||||
|
return llm_response
|
||||||
|
|
||||||
|
def generate_quest(self, npc_name, player_level):
|
||||||
|
npc = self.npcs.get(npc_name)
|
||||||
|
if not npc:
|
||||||
|
return None
|
||||||
|
prompt = (
|
||||||
|
f"As {npc.name}, create a short quest for a level {player_level} adventurer.\n"
|
||||||
|
f"Personality: {npc.personality}\n"
|
||||||
|
f"Backstory: {npc.backstory}\n"
|
||||||
|
f"Format as JSON:\n"
|
||||||
|
"{{\n"
|
||||||
|
' "title": "Quest name",\n'
|
||||||
|
' "description": "What the player must do",\n'
|
||||||
|
' "reward": "coins/items/reputation",\n'
|
||||||
|
' "difficulty": "easy/medium/hard"\n'
|
||||||
|
"}}"
|
||||||
|
)
|
||||||
|
llm_response = query_llm(prompt)
|
||||||
|
try:
|
||||||
|
if not llm_response:
|
||||||
|
raise ValueError("No response from LLM")
|
||||||
|
quest = json.loads(llm_response)
|
||||||
|
return quest
|
||||||
|
except Exception:
|
||||||
|
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
|
||||||
@@ -43,4 +43,31 @@ class NPCMemory:
|
|||||||
else:
|
else:
|
||||||
cur.execute('INSERT INTO npc_relationships (npc_id, player_id, affinity, last_interaction) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
|
cur.execute('INSERT INTO npc_relationships (npc_id, player_id, affinity, last_interaction) VALUES (?, ?, ?, CURRENT_TIMESTAMP)',
|
||||||
(npc_id, player_id, delta))
|
(npc_id, player_id, delta))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
def get_conversation(self, npc_id, player_id, limit=3):
|
||||||
|
"""Return the last `limit` (default 3) (player_msg, npc_reply) tuples for this NPC/player."""
|
||||||
|
cursor = self.conn.execute(
|
||||||
|
'''
|
||||||
|
SELECT message, response FROM npc_conversations
|
||||||
|
WHERE npc_id = ? AND player_id = ?
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT ?
|
||||||
|
''',
|
||||||
|
(npc_id, player_id, limit)
|
||||||
|
)
|
||||||
|
# Return in chronological order (oldest first)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
return rows[::-1]
|
||||||
|
|
||||||
|
def get_affinity(self, npc_id, player_id):
|
||||||
|
"""Return the affinity value for this NPC/player, or 0 if not set."""
|
||||||
|
cursor = self.conn.execute(
|
||||||
|
'''
|
||||||
|
SELECT affinity FROM npc_relationships
|
||||||
|
WHERE npc_id = ? AND player_id = ?
|
||||||
|
''',
|
||||||
|
(npc_id, player_id)
|
||||||
|
)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return row[0] if row else 0
|
||||||
Reference in New Issue
Block a user