import json
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
class ShaiyaQuestEditor(tk.Tk):
def __init__(self):
super().__init__()
self.title("Shaiya Quest Editor - Dark Mode")
self.geometry("1200x900")
# Theme colors
self.dark_bg = '#2e2e2e'
self.dark_fg = '#ffffff'
self.field_bg = '#3e3e3e'
self.highlight = '#444444'
self.configure(bg=self.dark_bg, padx=10, pady=10)
# Apply dark theme to ttk
style = ttk.Style(self)
style.theme_use('clam')
style.configure('.', background=self.dark_bg, foreground=self.dark_fg)
style.configure('TFrame', background=self.dark_bg)
style.configure('TLabelFrame', background=self.dark_bg, foreground=self.dark_fg)
style.configure('TLabel', background=self.dark_bg, foreground=self.dark_fg)
style.configure('TEntry', fieldbackground=self.field_bg, background=self.field_bg, foreground=self.dark_fg)
style.configure('TCheckbutton', background=self.dark_bg, foreground=self.dark_fg)
style.configure('TButton', background=self.highlight, foreground=self.dark_fg)
style.map('TButton', background=[('active', '#5e5e5e')])
self.data = None
self.current_quest = None
self.file_path = None
self.build_ui()
def create_tooltip(self, widget, text):
tooltip = tk.Toplevel(widget)
tooltip.withdraw()
tooltip.wm_overrideredirect(True)
label = tk.Label(tooltip, text=text, background=self.highlight, foreground=self.dark_fg, relief="solid", borderwidth=1, padx=5, pady=3)
label.pack()
def on_enter(event):
x = widget.winfo_rootx() + 20
y = widget.winfo_rooty() + widget.winfo_height() + 5
tooltip.geometry(f"+{x}+{y}")
tooltip.deiconify()
def on_leave(event):
tooltip.withdraw()
widget.bind("<Enter>", on_enter)
widget.bind("<Leave>", on_leave)
def style_text(self, txt):
txt.configure(bg=self.field_bg, fg=self.dark_fg, insertbackground=self.dark_fg, wrap='word')
txt.tag_configure('center', justify='center')
def build_ui(self):
# Top controls
top = ttk.Frame(self)
top.pack(fill="x", pady=(0, 10))
ttk.Button(top, text="Charger JSON", command=self.load_json).pack(side="left")
ttk.Label(top, text="Quest ID:").pack(side="left", padx=(20, 5))
self.quest_id_entry = ttk.Entry(top, width=8, justify='center')
self.quest_id_entry.pack(side="left")
ttk.Button(top, text="Afficher Quête", command=self.load_quest).pack(side="left", padx=(5, 0))
# Scrollable container
container = ttk.Frame(self)
container.pack(fill="both", expand=True)
canvas = tk.Canvas(container, bg=self.dark_bg, highlightthickness=0)
scrollbar = ttk.Scrollbar(container, orient="vertical", command=canvas.yview)
self.scrollable_frame = ttk.Frame(canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Quest Info section
info_frame = ttk.LabelFrame(self.scrollable_frame, text="Quest Info", padding=(10, 10))
info_frame.pack(fill="x", pady=(0, 10))
self.info_fields = {}
info_labels = [
("Name:", "name", "Nom de la quête"),
("Type:", "questType", "Type de quête (PVE/PVP/Collecte/Dialogue)"),
("Source:", "obtained", "Origine de la quête (NPC, Item...)"),
("Faction:", "faction", "Faction ciblée (Any, Light, Fury...)"),
("Mode:", "mode", "Difficulté de la quête"),
]
for idx, (lbl, key, tip) in enumerate(info_labels):
ttk.Label(info_frame, text=lbl).grid(row=0, column=idx*2, sticky="w", padx=(0,5))
entry = ttk.Entry(info_frame, width=18, justify='center')
entry.grid(row=0, column=idx*2+1, sticky="w", padx=(0,15))
self.info_fields[key] = entry
self.create_tooltip(entry, tip)
# Classes section
class_frame = ttk.LabelFrame(self.scrollable_frame, text="Classes Autorisées", padding=(10, 10))
class_frame.pack(fill="x", pady=(0, 10))
self.class_vars = {}
classes = ["Fighter", "Defender", "Ranger", "Archer", "Mage", "Priest"]
for i, cls in enumerate(classes):
var = tk.BooleanVar()
cb = ttk.Checkbutton(class_frame, text=cls, variable=var)
cb.grid(row=0, column=i, padx=5, sticky="w")
self.class_vars[cls.lower()] = var
self.create_tooltip(cb, f"Autoriser la classe {cls}")
# Settings section
setting_frame = ttk.LabelFrame(self.scrollable_frame, text="Settings", padding=(10, 10))
setting_frame.pack(fill="x", pady=(0, 10))
self.setting_fields = {}
settings = [
("Level Min", "minLevel", "Niveau minimum requis"),
("Level Max", "maxLevel", "Niveau maximum requis"),
("Start NPC ID", "startNpcId", "ID du PNJ de départ"),
("Start NPC Type", "startNpcType", "Type du PNJ de départ"),
("End NPC ID", "endNpcId", "ID du PNJ de fin"),
("End NPC Type", "endNpcType", "Type du PNJ de fin"),
("Requires Items", "requires", "Nombre d'items requis"),
]
for idx, (lbl, key, tip) in enumerate(settings):
ttk.Label(setting_frame, text=lbl).grid(row=0, column=idx*2, sticky="w", padx=(0,5))
entry = ttk.Entry(setting_frame, width=8, justify='center')
entry.grid(row=0, column=idx*2+1, sticky="w", padx=(0,15))
self.setting_fields[key] = entry
self.create_tooltip(entry, tip)
self.repeat_var = tk.BooleanVar()
cb = ttk.Checkbutton(setting_frame, text="Repeatable", variable=self.repeat_var)
cb.grid(row=0, column=len(settings)*2, padx=5)
self.create_tooltip(cb, "Quête répétable")
# Objectives section
obj_frame = ttk.LabelFrame(self.scrollable_frame, text="Objectives", padding=(10, 10))
obj_frame.pack(fill="x", pady=(0, 10))
ttk.Label(obj_frame, text="Received Items").grid(row=0, column=0, columnspan=3, sticky="w")
self.received_items = []
for r in range(3):
row_fields = {}
for c, key in enumerate(["type", "typeId", "count"]):
entry = ttk.Entry(obj_frame, width=6, justify='center')
entry.grid(row=r+1, column=c, padx=5, pady=2)
row_fields[key] = entry
self.create_tooltip(entry, f"{key} pour received item {r+1}")
self.received_items.append(row_fields)
ttk.Label(obj_frame, text="Items to Farm").grid(row=0, column=4, columnspan=3, sticky="w", padx=(20,0))
self.farm_items = []
for r in range(3):
row_fields = {}
for c, key in enumerate(["type", "typeId", "count"]):
entry = ttk.Entry(obj_frame, width=6, justify='center')
entry.grid(row=r+1, column=4+c, padx=5, pady=2)
row_fields[key] = entry
self.create_tooltip(entry, f"{key} pour farm item {r+1}")
self.farm_items.append(row_fields)
ttk.Label(obj_frame, text="Mobs to Fight").grid(row=0, column=8, sticky="w", padx=(20,0))
self.mob_type = ttk.Entry(obj_frame, width=6, justify='center')
self.mob_type.grid(row=1, column=8, padx=5, pady=2)
self.create_tooltip(self.mob_type, "ID du mob à tuer")
self.mob_count = ttk.Entry(obj_frame, width=6, justify='center')
self.mob_count.grid(row=1, column=9, padx=5, pady=2)
self.create_tooltip(self.mob_count, "Nombre de mobs à tuer")
ttk.Label(obj_frame, text="PvP Kills").grid(row=0, column=10, sticky="w", padx=(20,0))
self.pvp_kills = ttk.Entry(obj_frame, width=6, justify='center')
self.pvp_kills.grid(row=1, column=10, padx=5, pady=2)
self.create_tooltip(self.pvp_kills, "Nombre de kills en PvP")
# Descriptions & Messages section
desc_frame = ttk.LabelFrame(self.scrollable_frame, text="Descriptions & Messages", padding=(10, 10))
desc_frame.pack(fill="both", expand=True, pady=(0,10))
self.desc_fields = {}
descs = [
("Welcome Msg", "welcomeMsg"),
("Initial Description", "initialDescription"),
("Summary", "summary"),
("Reminder Instructions", "reminderInstructions"),
("Alternate Response", "alternateResponse"),
]
for i, (lbl, key) in enumerate(descs):
ttk.Label(desc_frame, text=lbl).grid(row=i, column=0, sticky="nw", pady=2)
txt = tk.Text(desc_frame, height=3, width=100)
self.style_text(txt)
txt.grid(row=i, column=1, padx=5, pady=2)
self.desc_fields[key] = txt
self.create_tooltip(txt, f"Champ {lbl}")
for j in range(6):
lbl = f"Completion Msg {j+1}" if j==0 else f"Alternate Completion Msg {j+1}"
key = "completionMessage" + ("" if j==0 else str(j+1))
ttk.Label(desc_frame, text=lbl).grid(row=len(descs)+j, column=0, sticky="nw", pady=2)
txt = tk.Text(desc_frame, height=2, width=100)
self.style_text(txt)
txt.grid(row=len(descs)+j, column=1, padx=5, pady=2)
self.desc_fields[key] = txt
self.create_tooltip(txt, f"Message de complétion #{j+1}")
# Rewards section
rew_frame = ttk.LabelFrame(self.scrollable_frame, text="Rewards", padding=(10, 10))
rew_frame.pack(fill="both", expand=True)
headers = ["Use", "MobCnt", "ItemCnt", "Gold", "XP",
"Type1", "TypeID1", "Count1",
"Type2", "TypeID2", "Count2",
"Type3", "TypeID3", "Count3", "Unlock"]
for c, h in enumerate(headers):
ttk.Label(rew_frame, text=h).grid(row=0, column=c, padx=5, pady=2)
self.reward_rows = []
for r in range(6):
row_vars = {}
var = tk.BooleanVar()
cb = ttk.Checkbutton(rew_frame, variable=var)
cb.grid(row=r+1, column=0)
row_vars['use'] = var
for c, key in enumerate(headers[1:], start=1):
ent = ttk.Entry(rew_frame, width=6, justify='center')
ent.grid(row=r+1, column=c, padx=5, pady=2)
row_vars[key.lower()] = ent
self.create_tooltip(ent, f"Champ {key} ligne {r+1}")
self.reward_rows.append(row_vars)
self.default_var = tk.BooleanVar()
cbd = ttk.Checkbutton(rew_frame, text="Give default reward", variable=self.default_var)
cbd.grid(row=7, column=0, columnspan=3, pady=(5,0))
self.create_tooltip(cbd, "Activer la récompense par défaut")
# Save button
save_btn = ttk.Button(self, text="Sauvegarder Tout", command=self.save_quest)
save_btn.pack(pady=(10,0))
def load_json(self):
path = filedialog.askopenfilename(filetypes=[("JSON files","*.json")])
if not path: return
with open(path,'r',encoding='utf-8') as f:
self.data = json.load(f)
self.file_path = path
messagebox.showinfo("Chargé","Fichier JSON chargé avec succès.")
def load_quest(self):
if not self.data or "quests" not in self.data:
messagebox.showerror("Erreur","Fichier non chargé ou invalide.")
return
try:
qid = int(self.quest_id_entry.get())
except:
messagebox.showerror("Erreur","ID invalide.")
return
for quest in self.data["quests"]:
if quest.get("id") == qid:
self.current_quest = quest
break
else:
messagebox.showerror("Erreur","Quête non trouvée.")
return
# Fill fields
for key, ent in self.info_fields.items(): ent.delete(0, tk.END); ent.insert(0, str(self.current_quest.get(key, "")))
for cls, var in self.class_vars.items(): var.set(self.current_quest.get(cls, False))
for key, ent in self.setting_fields.items(): ent.delete(0, tk.END); ent.insert(0, str(self.current_quest.get(key, 0)))
self.repeat_var.set(self.current_quest.get("repeat able", False))
for idx, row in enumerate(self.received_items):
data = self.current_quest.get("requiredItems", [])
val = data[idx] if idx < len(data) else {"type":0,"typeId":0,"count":0}
for k, ent in row.items(): ent.delete(0, tk.END); ent.insert(0, str(val.get(k, 0)))
for idx, row in enumerate(self.farm_items):
data = self.current_quest.get("farmItems", [])
val = data[idx] if idx < len(data) else {"type":0,"typeId":0,"count":0}
for k, ent in row.items(): ent.delete(0, tk.END); ent.insert(0, str(val.get(k, 0)))
self.mob_type.delete(0, tk.END); self.mob_type.insert(0, str(self.current_quest.get("requiredMobId1", 0)))
self.mob_count.delete(0, tk.END); self.mob_count.insert(0, str(self.current_quest.get("requiredMobCount1", 0)))
self.pvp_kills.delete(0, tk.END); self.pvp_kills.insert(0, str(self.current_quest.get("pvpKillCount", 0)))
for key, txt in self.desc_fields.items(): txt.delete("1.0", tk.END); txt.insert("1.0", self.current_quest.get(key, "")); txt.tag_add('center', '1.0', 'end')
results = self.current_quest.get("results", [])
mapping = {
'mobcnt':'needMobCount','itemcnt':'needItemCount', 'gold':'money','xp':'exp',
'type1':'itemType1','typeid1':'itemTypeId1','count 1':'itemCount1',
'type2':'itemType2','typeid2':'itemTypeId2','count 2':'itemCount2',
'type3':'itemType3','typeid3':'itemTypeId3','count 3':'itemCount3','unlock':'nextQuestId'
}
for r, row in enumerate(self.reward_rows):
row['use'].set(r < len(results))
if r < len(results):
res = results[r]
for key, ent in row.items():
if key == 'use': continue
ent.delete(0, tk.END)
ent.insert(0, str(res.get(mapping.get(key, key), 0)))
self.default_var.set(False)
def save_quest(self):
if not self.current_quest: messagebox.showerror("Erreur","Aucune quête chargée."); return
for key, ent in self.info_fields.items(): self.current_quest[key] = ent.get()
for cls, var in self.class_vars.items(): self.current_quest[cls] = var.get()
for key, ent in self.setting_fields.items():
try: self.current_quest[key] = int(ent.get())
except: self.current_quest[key] = 0
self.current_quest['repeatable'] = self.repeat_var.get()
self.current_quest['requiredItems'] = [{k:int(row[k].get()) for k in row} for row in self.received_items]
self.current_quest['farmItems'] = [{k:int(row[k].get()) for k in row} for row in self.farm_items]
try: self.current_quest['requiredMobId1'] = int(self.mob_type.get())
except: self.current_quest['requiredMobId1'] = 0
try: self.current_quest['requiredMobCount1'] = int(self.mob_count.get())
except: self.current_quest['requiredMobCount1'] = 0
try: self.current_quest['pvpKillCount'] = int(self.pvp_kills.get())
except: self.current_quest['pvpKillCount'] = 0
for key, txt in self.desc_fields.items(): self.current_quest[key] = txt.get("1.0", tk.END).strip()
results = []
for row in self.reward_rows:
if row['use'].get():
res = { 'needMobCount': int(row['mobcnt'].get()), 'needItemCount': int(row['itemcnt'].get()), 'money': int(row['gold'].get()), 'exp': int(row['xp'].get()),
'itemType1': int(row['type1'].get()), 'itemTypeId1': int(row['typeid1'].get()), 'itemCount1': int(row['count1'].get()),
'itemType2': int(row['type2'].get()), 'itemTypeId2': int(row['typeid2'].get()), 'itemCount2': int(row['count2'].get()),
'itemType3': int(row['type3'].get()), 'itemTypeId3': int(row['typeid3'].get()), 'itemCount3': int(row['count3'].get()),
'nextQuestId': int(row['unlock'].get()) }
results.append(res)
self.current_quest['results'] = results
with open(self.file_path,'w',encoding='utf-8') as f:
json.dump(self.data,f,ensure_ascii=False,indent=4)
messagebox.showinfo("Succès","Quête sauvegardée avec succès.")
if __name__ == "__main__":
app = ShaiyaQuestEditor()
app.mainloop()






