İlk commit: Stepstead backend (FastAPI)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jts 2026-07-11 11:30:24 +03:00
commit ea049b8ccd
45 changed files with 6751 additions and 0 deletions

120
db/export_crafts_json.py Normal file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""
Stepstead craft veritabanı JSON üretici.
recipes + items /var/www/stepstead.com/db/data/crafts.json
Grup: tarif kodu önekinden. Aile: kod son ekinden (malzeme ailesi).
Çalıştırma: /opt/stepstead/venv/bin/python3 /opt/stepstead/db/export_crafts_json.py
"""
import asyncio
import json
import sys
import asyncpg
sys.path.insert(0, "/opt/stepstead")
from config import settings
DSN = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
OUT = "/var/www/stepstead.com/db/data/crafts.json"
GROUPS = [
("saw_", "Kerestecilik"), ("build_", "Marangozluk"),
("craft_bow_", "Okçuluk"), ("craft_arrow_", "Okçuluk"),
("craft_staff_", "Büyücülük"),
("smelt_", "Metalurji"), ("forge_", "Demircilik"),
("craft_axe_", "Demircilik"), ("craft_pick_", "Demircilik"),
("jewel_", "Kuyumculuk"), ("brew_", "Simya"),
("cook_", "Aşçılık"), ("tailor_", "Dericilik"), ("craft_rod", "Balıılık"),
]
FAMILY = {
# odunlar
"oak": "Meşe", "pine": "Çam", "birch": "Huş", "beech": "Kayın", "maple": "Akçaağaç",
"willow": "Söğüt", "ash": "Dişbudak", "cedar": "Sedir", "walnut": "Ceviz",
"cherry": "Kiraz", "yew": "Porsuk", "mahogany": "Maun", "ebony": "Abanoz",
# metaller
"copper": "Bakır", "bronze": "Bronz", "iron": "Demir", "steel": "Çelik",
"silver": "Gümüş", "gold": "Altın", "cu": "Bakır", "fe": "Demir", "stone": "Taş",
# otlar
"mint": "Nane", "sage": "Adaçayı", "thyme": "Kekik", "chamomile": "Papatya",
"rosemary": "Biberiye", "basil": "Fesleğen", "nettle": "Isırgan",
"lemonbalm": "Melisa", "fennel": "Rezene", "lavender": "Lavanta",
"ginseng": "Ginseng", "saffron": "Safran",
# balıklar
"carp": "Sazan", "perch": "Levrek", "anchovy": "Hamsi", "mullet": "Kefal",
"mackerel": "Uskumru", "bonito": "Palamut", "bluefish": "Lüfer", "bream": "Çipura",
"trout": "Alabalık", "pike": "Turna", "catfish": "Yayın", "salmon": "Somon",
"turbot": "Kalkan", "tuna": "Ton Balığı", "sturgeon": "Mersin",
# hayvanlar
"rabbit": "Tavşan", "deer": "Geyik", "boar": "Yaban Domuzu", "wolf": "Kurt", "bear": "Ayı",
}
SKILL_TR = {
"mining": "Madencilik", "fishing": "Balıılık", "foraging": "Toplama",
"woodcutting": "Ağaç Kesme", "hunting": "Avcılık", "combat": "Savaş",
"smithing": "Demircilik", "alchemy": "Simya", "cooking": "Aşçılık",
"construction": "İnşaat",
}
def group_for(code: str) -> str:
for prefix, g in GROUPS:
if code.startswith(prefix):
return g
return "Diğer"
def family_for(code: str) -> str:
return FAMILY.get(code.rsplit("_", 1)[-1], "Genel")
async def main():
conn = await asyncpg.connect(DSN)
item_rows = await conn.fetch("SELECT code, name, rarity, type, effect FROM items")
imap = {r["code"]: dict(r) for r in item_rows}
rows = await conn.fetch("""
SELECT r.code, r.name, r.output_qty, r.skill, r.required_level,
r.xp_reward, r.step_cost, r.ingredients, i.code AS out_code
FROM recipes r JOIN items i ON i.id = r.output_item_id
ORDER BY r.required_level, r.name
""")
await conn.close()
out = []
for r in rows:
oi = imap.get(r["out_code"], {})
effect = oi.get("effect")
if isinstance(effect, str):
effect = json.loads(effect)
ings = json.loads(r["ingredients"]) if isinstance(r["ingredients"], str) else r["ingredients"]
out.append({
"code": r["code"],
"name": r["name"],
"group": group_for(r["code"]),
"family": family_for(r["code"]),
"skill": SKILL_TR.get(r["skill"], r["skill"]),
"level": r["required_level"],
"steps": r["step_cost"],
"xp": r["xp_reward"],
"out_qty": r["output_qty"],
"rarity": oi.get("rarity", "common"),
"type": oi.get("type", ""),
"effect": effect,
"ings": [
{"name": imap.get(g["item_code"], {}).get("name", g["item_code"]),
"qty": g["qty"],
"rarity": imap.get(g["item_code"], {}).get("rarity", "common")}
for g in ings
],
})
groups: dict[str, int] = {}
for o in out:
groups[o["group"]] = groups.get(o["group"], 0) + 1
with open(OUT, "w", encoding="utf-8") as f:
json.dump({"recipes": out, "groups": groups, "total": len(out)}, f, ensure_ascii=False)
print(f"{OUT}: {len(out)} tarif, gruplar: {groups}")
asyncio.run(main())

109
db/export_items_json.py Normal file
View file

@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
Stepstead eşya veritabanı JSON üretici.
items + item_classes + location_resources + recipes items.json
Her eşya için: tür, rarity, yuva, sınıf kullanım kısıtı, kaynak (nereden toplanır/üretilir).
Çalıştırma: /opt/stepstead/venv/bin/python3 /opt/stepstead/db/export_items_json.py
"""
import asyncio
import json
import sys
import asyncpg
sys.path.insert(0, "/opt/stepstead")
from config import settings
DSN = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
OUT = "/var/www/stepstead.com/items/data/items.json"
TYPE_TR = {
"material": "Ham Madde", "tool": "Alet", "gear": "Ekipman",
"consumable": "Tüketilebilir", "furniture": "Mobilya",
}
SLOT_TR = {
"weapon": "Silah", "shield": "Kalkan", "chest": "Gövde", "head": "Baş",
"hands": "Eldiven", "feet": "Ayak", "waist": "Bel", "back": "Sırt",
"neck": "Boyun", "finger": "Yüzük", "wrist": "Bilek", "tool": "Alet",
}
SKILL_TR = {
"mining": "Madencilik", "fishing": "Balıılık", "foraging": "Toplama",
"woodcutting": "Ağaç Kesme", "hunting": "Avcılık", "combat": "Savaş",
"smithing": "Demircilik", "alchemy": "Simya", "cooking": "Aşçılık",
"construction": "İnşaat",
}
async def main():
conn = await asyncpg.connect(DSN)
items = await conn.fetch("""
SELECT id, code, name, type, rarity, equip_slot, two_handed,
base_value, effect, description
FROM items ORDER BY type, rarity, name
""")
# sınıf kullanım kısıtları
ic = await conn.fetch("""
SELECT ic.item_id, c.name AS class_name, c.sort_order
FROM item_classes ic JOIN game_classes c ON c.id = ic.class_id
ORDER BY c.sort_order, c.name
""")
classes_by_item: dict[int, list[str]] = {}
for r in ic:
classes_by_item.setdefault(r["item_id"], []).append(r["class_name"])
all_classes = [dict(r) for r in await conn.fetch(
"SELECT name FROM game_classes ORDER BY sort_order, name")]
all_class_names = [c["name"] for c in all_classes]
# kaynak: nereden toplanır (bölge işaretleme)
gathers = await conn.fetch("""
SELECT lr.item_code, l.name AS loc, lr.skill
FROM location_resources lr JOIN locations l ON l.id = lr.location_id
ORDER BY l.name
""")
gather_by_code: dict[str, list[dict]] = {}
for r in gathers:
gather_by_code.setdefault(r["item_code"], []).append(
{"loc": r["loc"], "skill": SKILL_TR.get(r["skill"], r["skill"])})
# kaynak: üretilebilir mi
craftable = set(r["code"] for r in await conn.fetch("""
SELECT i.code FROM recipes r JOIN items i ON i.id = r.output_item_id
"""))
await conn.close()
out = []
for r in items:
eff = r["effect"]
if isinstance(eff, str):
eff = json.loads(eff)
cls = classes_by_item.get(r["id"], [])
# kayıt yoksa ve takılabilir bir eşyaysa: herkes; değilse boş
equip = r["equip_slot"]
out.append({
"code": r["code"],
"name": r["name"],
"type": TYPE_TR.get(r["type"], r["type"]),
"type_code": r["type"],
"rarity": r["rarity"],
"slot": SLOT_TR.get(equip) if equip else None,
"slot_code": equip,
"two_handed": r["two_handed"],
"value": r["base_value"],
"effect": eff,
"desc": r["description"],
"classes": cls, # boş = tüm sınıflar
"gather": gather_by_code.get(r["code"], []),
"craftable": r["code"] in craftable,
})
types: dict[str, int] = {}
for o in out:
types[o["type"]] = types.get(o["type"], 0) + 1
with open(OUT, "w", encoding="utf-8") as f:
json.dump({"items": out, "types": types, "classes": all_class_names,
"total": len(out)}, f, ensure_ascii=False)
print(f"{OUT}: {len(out)} eşya, türler: {types}")
asyncio.run(main())

196
db/init_schema.sql Normal file
View file

@ -0,0 +1,196 @@
-- stepstead.com — initial schema (MVP)
-- Idempotent: safe to re-run.
BEGIN;
-- =========================================================================
-- updated_at trigger
-- =========================================================================
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- =========================================================================
-- 1. AUTH / KULLANICI
-- =========================================================================
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(64),
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
email_verify_token VARCHAR(128),
email_verify_sent_at TIMESTAMPTZ,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
last_login_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_users_email_verify_token ON users(email_verify_token)
WHERE email_verify_token IS NOT NULL;
DROP TRIGGER IF EXISTS trg_users_updated_at ON users;
CREATE TRIGGER trg_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
CREATE TABLE IF NOT EXISTS password_reset_tokens (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(128) NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_pwreset_user ON password_reset_tokens(user_id);
-- =========================================================================
-- 2. KARAKTER
-- =========================================================================
CREATE TABLE IF NOT EXISTS characters (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(32) NOT NULL UNIQUE,
level INTEGER NOT NULL DEFAULT 1 CHECK (level BETWEEN 1 AND 50),
total_xp BIGINT NOT NULL DEFAULT 0,
gold BIGINT NOT NULL DEFAULT 0,
step_balance BIGINT NOT NULL DEFAULT 0,
step_lifetime BIGINT NOT NULL DEFAULT 0,
pos_chunk_x INTEGER NOT NULL DEFAULT 0,
pos_chunk_y INTEGER NOT NULL DEFAULT 0,
pos_x INTEGER NOT NULL DEFAULT 0,
pos_y INTEGER NOT NULL DEFAULT 0,
last_sync_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_characters_user ON characters(user_id);
DROP TRIGGER IF EXISTS trg_characters_updated_at ON characters;
CREATE TRIGGER trg_characters_updated_at BEFORE UPDATE ON characters
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =========================================================================
-- 3. SKILL (hibrit: her skill ayrı XP, karakter level = toplam)
-- =========================================================================
CREATE TABLE IF NOT EXISTS skills (
character_id BIGINT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
skill_name VARCHAR(32) NOT NULL,
xp BIGINT NOT NULL DEFAULT 0,
level INTEGER NOT NULL DEFAULT 1 CHECK (level BETWEEN 1 AND 50),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (character_id, skill_name)
);
DROP TRIGGER IF EXISTS trg_skills_updated_at ON skills;
CREATE TRIGGER trg_skills_updated_at BEFORE UPDATE ON skills
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- =========================================================================
-- 4. ITEM CATALOG + ENVANTER
-- =========================================================================
CREATE TABLE IF NOT EXISTS items (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(128) NOT NULL,
type VARCHAR(32) NOT NULL,
rarity VARCHAR(16) NOT NULL DEFAULT 'common',
stack_max INTEGER NOT NULL DEFAULT 99,
icon VARCHAR(128),
description TEXT
);
CREATE TABLE IF NOT EXISTS inventory (
id BIGSERIAL PRIMARY KEY,
character_id BIGINT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
item_id BIGINT NOT NULL REFERENCES items(id),
qty INTEGER NOT NULL DEFAULT 1 CHECK (qty > 0),
slot INTEGER NOT NULL,
UNIQUE (character_id, slot)
);
CREATE INDEX IF NOT EXISTS idx_inventory_char ON inventory(character_id);
-- =========================================================================
-- 5. HARITA (chunk + node)
-- =========================================================================
CREATE TABLE IF NOT EXISTS map_chunks (
chunk_x INTEGER NOT NULL,
chunk_y INTEGER NOT NULL,
biome VARCHAR(32) NOT NULL,
tile_data JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (chunk_x, chunk_y)
);
CREATE TABLE IF NOT EXISTS node_spawns (
id BIGSERIAL PRIMARY KEY,
chunk_x INTEGER NOT NULL,
chunk_y INTEGER NOT NULL,
x INTEGER NOT NULL,
y INTEGER NOT NULL,
node_type VARCHAR(32) NOT NULL,
item_id BIGINT REFERENCES items(id),
skill_name VARCHAR(32) NOT NULL,
required_level INTEGER NOT NULL DEFAULT 1,
step_cost INTEGER NOT NULL DEFAULT 10,
xp_reward INTEGER NOT NULL DEFAULT 10,
respawn_at TIMESTAMPTZ,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE INDEX IF NOT EXISTS idx_node_chunk ON node_spawns(chunk_x, chunk_y) WHERE is_active;
-- =========================================================================
-- 6. ADIM LOG (anti-cheat audit)
-- =========================================================================
CREATE TABLE IF NOT EXISTS step_log (
id BIGSERIAL PRIMARY KEY,
character_id BIGINT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
ts TIMESTAMPTZ NOT NULL DEFAULT NOW(),
steps_delta INTEGER NOT NULL,
source VARCHAR(32) NOT NULL,
accepted BOOLEAN NOT NULL DEFAULT TRUE,
reason TEXT
);
CREATE INDEX IF NOT EXISTS idx_step_log_char_ts ON step_log(character_id, ts DESC);
-- =========================================================================
-- 7. PAZAR + TAKAS
-- =========================================================================
CREATE TABLE IF NOT EXISTS market_listings (
id BIGSERIAL PRIMARY KEY,
seller_id BIGINT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
item_id BIGINT NOT NULL REFERENCES items(id),
qty INTEGER NOT NULL CHECK (qty > 0),
unit_price BIGINT NOT NULL CHECK (unit_price > 0),
status VARCHAR(16) NOT NULL DEFAULT 'open',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
closed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_market_open ON market_listings(item_id, unit_price) WHERE status='open';
CREATE TABLE IF NOT EXISTS trade_logs (
id BIGSERIAL PRIMARY KEY,
seller_id BIGINT NOT NULL REFERENCES characters(id),
buyer_id BIGINT NOT NULL REFERENCES characters(id),
item_id BIGINT NOT NULL REFERENCES items(id),
qty INTEGER NOT NULL,
unit_price BIGINT NOT NULL,
ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- =========================================================================
-- 8. SOHBET
-- =========================================================================
CREATE TABLE IF NOT EXISTS chat_messages (
id BIGSERIAL PRIMARY KEY,
channel VARCHAR(32) NOT NULL,
sender_id BIGINT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
text TEXT NOT NULL,
ts TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_chat_channel_ts ON chat_messages(channel, ts DESC);
COMMIT;

View file

@ -0,0 +1,33 @@
-- 2026-07-04: Market/vendor ekonomisi + lokasyona özel olaylar + günlük hedef/streak
-- İdempotent — tekrar çalıştırılabilir.
-- Vendor fiyatlandırması: rarity bazlı taban değer (gold)
ALTER TABLE items ADD COLUMN IF NOT EXISTS base_value INTEGER NOT NULL DEFAULT 5;
UPDATE items SET base_value = CASE rarity
WHEN 'common' THEN 5
WHEN 'uncommon' THEN 15
WHEN 'rare' THEN 40
WHEN 'epic' THEN 120
WHEN 'legendary' THEN 400
ELSE 5 END;
-- Tool'lar hammaddeden değerli olsun
UPDATE items SET base_value = base_value * 3 WHERE type = 'tool';
-- Olaylar lokasyona hedeflenebilir (NULL = her yerde çıkabilir).
-- Eşleşme: seyahatin VARIŞ lokasyon kodu listede ise.
ALTER TABLE events ADD COLUMN IF NOT EXISTS location_codes TEXT[];
-- Günlük hedef + streak
ALTER TABLE characters ADD COLUMN IF NOT EXISTS daily_steps BIGINT NOT NULL DEFAULT 0;
ALTER TABLE characters ADD COLUMN IF NOT EXISTS daily_date DATE;
ALTER TABLE characters ADD COLUMN IF NOT EXISTS streak_days INTEGER NOT NULL DEFAULT 0;
ALTER TABLE characters ADD COLUMN IF NOT EXISTS last_claim_date DATE;
-- Kamp/construction: karakter başına lokasyonda bir kamp (gather adım maliyeti -%25)
CREATE TABLE IF NOT EXISTS camps (
id BIGSERIAL PRIMARY KEY,
character_id BIGINT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
location_id BIGINT NOT NULL REFERENCES locations(id),
built_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(character_id, location_id)
);

View file

@ -0,0 +1,25 @@
-- Rol tabanlı yetkilendirme (2026-07-07)
-- İzin kodları: upload, lore, map_view, map_edit, users ("*" = hepsi)
BEGIN;
CREATE TABLE IF NOT EXISTS roles (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(40) UNIQUE NOT NULL,
permissions JSONB NOT NULL DEFAULT '[]',
is_system BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO roles(name, permissions, is_system) VALUES
('admin', '["*"]', TRUE),
('design', '["upload"]', FALSE),
('loremaster', '["lore","map_view"]', FALSE)
ON CONFLICT (name) DO NOTHING;
ALTER TABLE users ADD COLUMN IF NOT EXISTS role_id BIGINT REFERENCES roles(id) ON DELETE SET NULL;
-- Mevcut admin hesapları (ADMIN_EMAILS ile aynı) admin rolüne bağla
UPDATE users SET role_id = (SELECT id FROM roles WHERE name='admin')
WHERE email IN ('jts@jts.rocks','afm@jts.rocks') AND role_id IS NULL;
COMMIT;

View file

@ -0,0 +1,17 @@
-- Stepstead — Ekipman yuvası + sınıf kullanım kısıtı (2026-07-08)
-- items.equip_slot : eşya hangi yuvaya takılır (weapon/shield/head/chest/hands/feet/waist/back/neck/finger/wrist/tool/none)
-- items.two_handed : çift el silah (yay, asa) — yan-el (kalkan) engeller
-- item_classes : eşyayı KULLANABİLEN sınıflar. Kayıt YOKSA = tüm sınıflar kullanabilir (tool, mücevher, deri, tüketilebilir).
BEGIN;
ALTER TABLE items ADD COLUMN IF NOT EXISTS equip_slot varchar(16);
ALTER TABLE items ADD COLUMN IF NOT EXISTS two_handed boolean NOT NULL DEFAULT false;
CREATE TABLE IF NOT EXISTS item_classes (
item_id bigint NOT NULL REFERENCES items(id) ON DELETE CASCADE,
class_id int NOT NULL REFERENCES game_classes(id) ON DELETE CASCADE,
PRIMARY KEY (item_id, class_id)
);
CREATE INDEX IF NOT EXISTS idx_item_classes_class ON item_classes(class_id);
COMMIT;

View file

@ -0,0 +1,24 @@
-- Vendor NPC'leri şehirlere (locations) bağla + oyuncu konsinye stoğu (24s ömürlü).
-- 1) NPC → konum bağı
ALTER TABLE npcs ADD COLUMN IF NOT EXISTS location_id BIGINT REFERENCES locations(id) ON DELETE SET NULL;
-- 7 vendor'ı 3 şehre dağıt (koy=1, pazar=2, liman=3)
UPDATE npcs SET location_id=1 WHERE id IN (4,5,6); -- Köy: Demirci Baldor, Şifacı Elda, Oduncu Corvin
UPDATE npcs SET location_id=2 WHERE id IN (7,9); -- Pazarköy: Demirci Torin, Şifacı Mira
UPDATE npcs SET location_id=3 WHERE id IN (8,10); -- Liman: Balıı Neyla, Demirci Grum
-- 2) Konsinye stok: oyuncuların vendor'a sattığı ürünler, herkese satılık, 24s ömürlü.
-- Vendor'ın sabit stoğu = npc_items (kalıcı). Bu tablo geçici oyuncu malları.
CREATE TABLE IF NOT EXISTS vendor_stock (
id BIGSERIAL PRIMARY KEY,
npc_id BIGINT NOT NULL REFERENCES npcs(id) ON DELETE CASCADE,
item_id BIGINT NOT NULL REFERENCES items(id) ON DELETE CASCADE,
qty INTEGER NOT NULL CHECK (qty > 0),
unit_price INTEGER NOT NULL, -- alıcının ödeyeceği gold (vendor fiyatı)
expires_at TIMESTAMPTZ NOT NULL, -- bu andan sonra kaybolur (satışta yenilenir)
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (npc_id, item_id)
);
CREATE INDEX IF NOT EXISTS vendor_stock_npc_idx ON vendor_stock(npc_id);
CREATE INDEX IF NOT EXISTS vendor_stock_exp_idx ON vendor_stock(expires_at);

View file

@ -0,0 +1,3 @@
-- Yol geometrisi: iki lokasyon arası kıvrımlı hat için ara noktalar.
-- waypoints = normalize (0-1) [[x,y],...] — uç noktalar (loc_a, loc_b) hariç.
ALTER TABLE roads ADD COLUMN IF NOT EXISTS waypoints JSONB NOT NULL DEFAULT '[]'::jsonb;

244
db/seed_craftables.py Normal file
View file

@ -0,0 +1,244 @@
"""
250 örnek craftable üretir: item + tarif. Idempotent (code üzerinden ON CONFLICT DO NOTHING).
Gruplar: Kerestecilik, Marangozluk, Okçuluk, Metalurji, Demircilik, Kuyumculuk,
Simya, Aşçılık, Dericilik, Balıılık.
Çalıştırma: cd /opt/stepstead && ./venv/bin/python3 db/seed_craftables.py
"""
import asyncio
import json
import sys
import asyncpg
sys.path.insert(0, "/opt/stepstead")
from config import settings
DSN = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
# (kod, Türkçe ad, tier)
WOODS = [
("oak", "Meşe", 1), ("pine", "Çam", 2), ("birch", "Huş", 3), ("beech", "Kayın", 4),
("maple", "Akçaağaç", 5), ("willow", "Söğüt", 6), ("ash", "Dişbudak", 7), ("cedar", "Sedir", 8),
("walnut", "Ceviz", 9), ("cherry", "Kiraz", 10), ("yew", "Porsuk", 11),
("mahogany", "Maun", 12), ("ebony", "Abanoz", 13),
]
METALS = [
("copper", "Bakır", 1), ("bronze", "Bronz", 2), ("iron", "Demir", 3),
("steel", "Çelik", 4), ("silver", "Gümüş", 5), ("gold", "Altın", 6),
]
HERBS = [
("mint", "Nane", 1), ("sage", "Adaçayı", 2), ("thyme", "Kekik", 3), ("chamomile", "Papatya", 4),
("rosemary", "Biberiye", 5), ("basil", "Fesleğen", 6), ("nettle", "Isırgan", 7),
("lemonbalm", "Melisa", 8), ("fennel", "Rezene", 9), ("lavender", "Lavanta", 10),
("ginseng", "Ginseng", 11), ("saffron", "Safran", 12),
]
FISH = [
("carp", "Sazan", 1), ("perch", "Levrek", 2), ("anchovy", "Hamsi", 3), ("mullet", "Kefal", 4),
("mackerel", "Uskumru", 5), ("bonito", "Palamut", 6), ("bluefish", "Lüfer", 7),
("bream", "Çipura", 8), ("trout", "Alabalık", 9), ("pike", "Turna", 10),
("catfish", "Yayın", 11), ("salmon", "Somon", 12), ("turbot", "Kalkan", 13),
("tuna", "Ton Balığı", 14), ("sturgeon", "Mersin", 15),
]
ANIMALS = [
("rabbit", "Tavşan", 1), ("deer", "Geyik", 2), ("boar", "Yaban Domuzu", 3),
("wolf", "Kurt", 4), ("bear", "Ayı", 5),
]
FURNITURE = [("chair", "Sandalye", 3), ("table", "Masa", 4), ("bed", "Yatak", 6),
("cabinet", "Dolap", 5), ("door", "Kapı", 4), ("chest", "Sandık", 5)] # (kod, ad, kereste)
WEAPONS = [("axe", "Balta"), ("pickaxe", "Kazma"), ("sword", "Kılıç"),
("dagger", "Hançer"), ("shield", "Kalkan"), ("armor", "Zırh")]
JEWELRY = [("ring", "Yüzük"), ("necklace", "Kolye"), ("bracelet", "Bilezik")]
LEATHERGOODS = [("bag", "Çanta"), ("belt", "Kemer"), ("boots", "Bot"), ("gloves", "Eldiven")]
def rarity_for(tier: int, max_tier: int) -> str:
r = tier / max_tier
if r <= 0.25:
return "common"
if r <= 0.5:
return "uncommon"
if r <= 0.75:
return "rare"
if r < 1.0:
return "epic"
return "legendary"
items: list[dict] = [] # {code,name,type,rarity,stack_max,description,effect}
recipes: list[dict] = [] # {code,name,out,out_qty,skill,level,xp,steps,ings}
def add_item(code, name, typ, rarity="common", stack=99, desc=None, effect=None):
items.append({"code": code, "name": name, "type": typ, "rarity": rarity,
"stack": stack, "desc": desc, "effect": effect})
def add_recipe(code, name, out, skill, level, xp, steps, ings, out_qty=1):
recipes.append({"code": code, "name": name, "out": out, "out_qty": out_qty,
"skill": skill, "level": min(level, 50), "xp": xp, "steps": steps,
"ings": [{"item_code": c, "qty": q} for c, q in ings]})
def build():
# --- Ham maddeler ---
for w, tr, t in WOODS:
add_item(f"log_{w}", f"{tr} Odunu", "material", rarity_for(t, 13))
for m, tr, t in METALS:
if m in ("bronze", "steel"):
pass # cevheri yok, alaşım
else:
add_item(f"ore_{m}", f"{tr} Cevheri", "material", rarity_for(t, 6))
add_item(f"ingot_{m}", f"{tr} Külçe", "material", rarity_for(t, 6))
add_item("ore_tin", "Kalay Cevheri", "material", "common")
add_item("coal", "Kömür", "material", "common")
for h, tr, t in HERBS:
add_item(f"herb_{h}", f"{tr}", "material", rarity_for(t, 12))
for f, tr, t in FISH:
add_item(f"fish_{f}", f"{tr}", "material", rarity_for(t, 15))
for a, tr, t in ANIMALS:
add_item(f"leather_{a}", f"{tr} Derisi", "material", rarity_for(t, 5))
add_item(f"meat_{a}", f"{tr} Eti", "material", rarity_for(t, 5))
# --- 1) Kerestecilik: 13 kereste ---
for w, tr, t in WOODS:
add_item(f"plank_{w}", f"{tr} Kereste", "material", rarity_for(t, 13))
add_recipe(f"saw_{w}", f"{tr} Kereste", f"plank_{w}", "woodcutting",
max(1, t * 3 - 2), 4 + 2 * t, 2 + t, [(f"log_{w}", 2)], out_qty=2)
# --- 2) Marangozluk: 13 odun × 6 mobilya = 78 ---
for w, wtr, t in WOODS:
for fc, ftr, planks in FURNITURE:
add_item(f"{fc}_{w}", f"{wtr} {ftr}", "furniture", rarity_for(t, 13), stack=10)
add_recipe(f"build_{fc}_{w}", f"{wtr} {ftr}", f"{fc}_{w}", "construction",
max(1, t * 3 - 2), 8 + 3 * t, 4 + t + planks, [(f"plank_{w}", planks)])
# --- 3) Okçuluk: 13 yay + 13 ok = 26 ---
for w, wtr, t in WOODS:
add_item(f"bow_{w}", f"{wtr} Yay", "tool", rarity_for(t, 13), stack=1)
add_recipe(f"craft_bow_{w}", f"{wtr} Yay", f"bow_{w}", "combat",
max(1, t * 3 - 2), 10 + 3 * t, 6 + t, [(f"plank_{w}", 2), ("vine", 1)])
add_item(f"arrow_{w}", f"{wtr} Ok", "material", rarity_for(t, 13))
add_recipe(f"craft_arrow_{w}", f"{wtr} Ok (10)", f"arrow_{w}", "combat",
max(1, t * 3 - 2), 5 + 2 * t, 3 + t, [(f"plank_{w}", 1), ("stone_small", 1)], out_qty=10)
# --- 4) Metalurji: 4 yeni külçe (bakır+demir eritme zaten var) ---
add_recipe("smelt_bronze", "Bronz Külçe", "ingot_bronze", "smithing", 4, 12, 5,
[("ore_copper", 1), ("ore_tin", 1)])
add_recipe("smelt_steel", "Çelik Külçe", "ingot_steel", "smithing", 10, 18, 6,
[("ore_iron", 2), ("coal", 1)])
add_recipe("smelt_silver", "Gümüş Külçe", "ingot_silver", "smithing", 15, 22, 7, [("ore_silver", 2)])
add_recipe("smelt_gold", "Altın Külçe", "ingot_gold", "smithing", 20, 28, 8, [("ore_gold", 2)])
# --- 5) Demircilik: 6 metal × 6 = 36, bakır balta+kazma mevcut → 34 ---
for m, mtr, t in METALS:
for wc, wtr in WEAPONS:
if m == "copper" and wc in ("axe", "pickaxe"):
continue # craft_axe_cu / craft_pick_cu zaten var
code = f"{wc}_{m}"
typ = "tool" if wc in ("axe", "pickaxe") else "gear"
add_item(code, f"{mtr} {wtr}", typ, rarity_for(t, 6), stack=1)
n_ingot = 3 if wc in ("shield", "armor") else 2
add_recipe(f"forge_{wc}_{m}", f"{mtr} {wtr}", code, "smithing",
max(1, t * 5 - 3), 10 + 5 * t, 6 + 2 * t,
[(f"ingot_{m}", n_ingot), ("log_oak", 1)])
# --- 6) Kuyumculuk: gümüş+altın × 3 takı = 6 ---
for m, mtr, t in [("silver", "Gümüş", 5), ("gold", "Altın", 6)]:
for jc, jtr in JEWELRY:
add_item(f"{jc}_{m}", f"{mtr} {jtr}", "gear", rarity_for(t, 6), stack=1)
add_recipe(f"jewel_{jc}_{m}", f"{mtr} {jtr}", f"{jc}_{m}", "smithing",
t * 5, 15 + 4 * t, 8, [(f"ingot_{m}", 1)])
# --- 7) Simya: 12 ot × 3 = 36, nane çayı mevcut → 35 ---
for h, htr, t in HERBS:
if h != "mint": # brew_tea_mint zaten var
add_item(f"tea_{h}", f"{htr} Çayı", "consumable", rarity_for(t, 12),
desc=f"Adım bakiyesini +{40 + 5 * t} yeniler.",
effect={"type": "steps", "amount": 40 + 5 * t})
add_recipe(f"brew_tea_{h}", f"{htr} Çayı", f"tea_{h}", "alchemy",
max(1, t * 4 - 3), 6 + 2 * t, 4, [(f"herb_{h}", 2)])
add_item(f"potion_{h}", f"{htr} İksiri", "consumable", rarity_for(t, 12),
desc=f"Adım bakiyesini +{80 + 10 * t} yeniler.",
effect={"type": "steps", "amount": 80 + 10 * t})
add_recipe(f"brew_potion_{h}", f"{htr} İksiri", f"potion_{h}", "alchemy",
min(50, t * 4), 12 + 3 * t, 6, [(f"herb_{h}", 3), ("vine", 1)])
add_item(f"salve_{h}", f"{htr} Merhemi", "consumable", rarity_for(t, 12),
desc="İyileştirici merhem (gelecek özellik).")
add_recipe(f"brew_salve_{h}", f"{htr} Merhemi", f"salve_{h}", "alchemy",
min(50, t * 4 + 1), 10 + 3 * t, 5, [(f"herb_{h}", 2), ("stone_small", 1)])
# --- 8) Aşçılık: 14 balık (sazan mevcut) + 5 et × 3 = 29 ---
for f, ftr, t in FISH:
if f == "carp":
continue # cook_carp zaten var
add_item(f"cooked_{f}", f"Izgara {ftr}", "consumable", rarity_for(t, 15),
desc=f"Adım bakiyesini +{25 + 3 * t} yeniler.",
effect={"type": "steps", "amount": 25 + 3 * t})
add_recipe(f"cook_{f}", f"Izgara {ftr}", f"cooked_{f}", "cooking",
max(1, t * 3 - 2), 6 + 2 * t, 4, [(f"fish_{f}", 1)])
for a, atr, t in ANIMALS:
for mc, mtr, bonus in [("grilled", "Izgara", 30), ("stew", "Güveç", 50), ("dried", "Kurutulmuş", 40)]:
add_item(f"{mc}_{a}", f"{mtr} {atr} Eti", "consumable", rarity_for(t, 5),
desc=f"Adım bakiyesini +{bonus + 8 * t} yeniler.",
effect={"type": "steps", "amount": bonus + 8 * t})
ings = [(f"meat_{a}", 1)] if mc != "stew" else [(f"meat_{a}", 1), ("herb_mint", 1)]
add_recipe(f"cook_{mc}_{a}", f"{mtr} {atr} Eti", f"{mc}_{a}", "cooking",
min(50, t * 6), 8 + 4 * t, 5, ings)
# --- 9) Dericilik: 5 deri × 4 = 20 ---
for a, atr, t in ANIMALS:
for lc, ltr in LEATHERGOODS:
add_item(f"{lc}_{a}", f"{atr} Derisi {ltr}", "gear", rarity_for(t, 5), stack=1)
add_recipe(f"tailor_{lc}_{a}", f"{atr} Derisi {ltr}", f"{lc}_{a}", "hunting",
min(50, t * 7 - 4), 10 + 5 * t, 6 + t, [(f"leather_{a}", 2), ("vine", 1)])
# --- 10) Balıılık: 2 gelişmiş olta ---
add_item("fishing_rod_fine", "Usta Oltası", "tool", "rare", stack=1)
add_recipe("craft_rod_fine", "Usta Oltası", "fishing_rod_fine", "fishing", 10, 25, 8,
[("plank_willow", 2), ("vine", 2)])
add_item("fishing_rod_master", "Efsane Olta", "tool", "epic", stack=1)
add_recipe("craft_rod_master", "Efsane Olta", "fishing_rod_master", "fishing", 25, 50, 12,
[("plank_yew", 2), ("vine", 3), ("ingot_silver", 1)])
# --- 11) İnşaat ekstra: 3 ---
add_item("campfire", "Kamp Ateşi", "furniture", "common", stack=10)
add_recipe("build_campfire", "Kamp Ateşi", "campfire", "construction", 1, 8, 5,
[("stick", 3), ("stone_small", 2)])
add_item("fence", "Çit", "furniture", "common", stack=50)
add_recipe("build_fence", "Çit", "fence", "construction", 2, 6, 4, [("plank_pine", 1), ("vine", 1)])
add_item("sign", "Tabela", "furniture", "common", stack=20)
add_recipe("build_sign", "Tabela", "sign", "construction", 1, 5, 3, [("plank_oak", 1), ("stick", 1)])
async def main():
build()
print(f"Üretilen: {len(recipes)} tarif, {len(items)} item tanımı")
assert len(recipes) == 250, f"250 bekleniyor, {len(recipes)} üretildi"
conn = await asyncpg.connect(DSN)
ins_i = 0
for it in items:
r = await conn.execute("""
INSERT INTO items (code, name, type, rarity, stack_max, description, effect)
VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT (code) DO NOTHING
""", it["code"], it["name"], it["type"], it["rarity"], it["stack"],
it["desc"], json.dumps(it["effect"]) if it["effect"] else None)
ins_i += int(r.endswith("1"))
ins_r = 0
for rc in recipes:
r = await conn.execute("""
INSERT INTO recipes (code, name, output_item_id, output_qty, skill, required_level,
xp_reward, step_cost, ingredients)
SELECT $1,$2,i.id,$3,$4,$5,$6,$7,$8::jsonb FROM items i WHERE i.code=$9
ON CONFLICT (code) DO NOTHING
""", rc["code"], rc["name"], rc["out_qty"], rc["skill"], rc["level"],
rc["xp"], rc["steps"], json.dumps(rc["ings"]), rc["out"])
ins_r += int(r.endswith("1"))
ti = await conn.fetchval("SELECT count(*) FROM items")
tr = await conn.fetchval("SELECT count(*) FROM recipes")
await conn.close()
print(f"DB'ye eklendi: {ins_i} item, {ins_r} tarif. Toplam: {ti} item, {tr} tarif.")
asyncio.run(main())

166
db/seed_enemies_forest.py Normal file
View file

@ -0,0 +1,166 @@
"""
Orman düşmanları 12 çeşit hostile NPC (2026-07-08).
NPC sayfasının yazdığı `npcs` tablosuna eklenir (stance='hostile').
Yer = forest region'larına (map_tags kind='region' category='forest', O-1..O-10)
round-robin dağıtılır. İsim'e göre idempotent (varsa günceller, yoksa ekler).
Çalıştırma:
cd /opt/stepstead && ./venv/bin/python3 db/seed_enemies_forest.py
"""
import asyncio
import sys
sys.path.insert(0, "/opt/stepstead")
import asyncpg # noqa: E402
from config import settings # noqa: E402
def desc(hp, dmg, atk, dc, weak, drops, danger, lore):
"""Özellikleri okunur bir description bloğuna paketle."""
return (
f"{lore}\n\n"
f"⚔ Saldırı türü: {atk}\n"
f"❤ Can (HP): {hp}\n"
f"🗡 Hasar: {dmg}\n"
f"🎯 Savaş zorluğu (combat DC): {dc}\n"
f"💀 Tehlike: {danger}\n"
f"🩹 Zayıflık: {weak}\n"
f"🎁 Düşürdükleri: {drops}"
)
# (name, role, stance, description)
ENEMIES = [
("Mana Solucanı", "Büyülü Yaratık", "hostile",
desc(hp=18, dmg="4-8 (mana yakımı)", atk="Büyü — mana emer",
dc=13, weak="Fiziksel darbe, demir silah",
drops="Mana özü ×1-2, parlak salgı",
danger="Orta",
lore="Yeraltı mana damarlarında beslenen, ışıldayan segmentli bir solucan. "
"Yaklaşana enerjisini emer; öldürüldüğünde içindeki saf mana damlar.")),
("Aç Kurt", "Canavar", "hostile",
desc(hp=22, dmg="6-10 (ısırık)", atk="Yakın dövüş — sürü halinde",
dc=13, weak="Ateş, yüksek zemin",
drops="Kurt eti ×1-2, kurt derisi ×1",
danger="Orta",
lore="Kışın açlıktan gözü dönmüş orman kurdu. Tek başına ürkek ama "
"sürüye katılınca korkusuzca saldırır.")),
("Yaşlı Ağaç Adam", "Bitki Devi", "hostile",
desc(hp=55, dmg="12-18 (dal savurma)", atk="Yakın dövüş — ağır ve yavaş",
dc=16, weak="Ateş, balta (woodcutting)",
drops="Kadim odun ×2-3, canlı kök, meşe palamudu",
danger="Yüksek",
lore="Yüzyıllık bir meşenin uyanmış ruhu. Toprağı sarsan adımlarla yürür; "
"yavaştır ama tek darbesi kemik kırar. Ateşten ölesiye korkar.")),
("Dev Orman Faresi", "Haşere", "hostile",
desc(hp=8, dmg="2-4 (kemirme)", atk="Yakın dövüş — kalabalık",
dc=9, weak="Hemen her şey",
drops="Fare kuyruğu, paslı çöp, bazen 1-3 gold",
danger="Düşük",
lore="İnsan boyunun yarısı kadar, hastalık taşıyan bir kemirgen. Tek başına "
"zararsız ama erzak çuvalını bir gecede boşaltır.")),
("Zehirli Orman Örümceği", "Haşere", "hostile",
desc(hp=16, dmg="5-9 (zehirli ısırık)", atk="Yakın dövüş — zehir + ağ",
dc=14, weak="Ateş, alan silahları",
drops="Örümcek zehri ×1, ipek ×1-2",
danger="Orta",
lore="Karanlık kovuklara dev ağlar geren, avını zehirle felç eden bir örümcek. "
"ı hareketi yavaşlatır, ısırığı damarlarda yanar.")),
("Sarmaşık Cini", "Bitki Ruhu", "hostile",
desc(hp=24, dmg="6-11 (sarma + diken)", atk="Yakın dövüş — kavrayıp sıkar",
dc=14, weak="Ateş, kesici balta",
drops="Canlı sarmaşık ×2, diken tozu, çiçek özü",
danger="Orta",
lore="Küçük ama sinsi bir bitki ruhu. Yaprakların arasına gizlenir, "
"geçeni sarmaşıklarıyla sarıp dikenlerine çeker.")),
("Mantar Spor Yaratığı", "Sporlu Canavar", "hostile",
desc(hp=20, dmg="4-7 (spor bulutu)", atk="Alan — zehirli spor patlaması",
dc=13, weak="Ateş, uzak menzil",
drops="ıldayan mantar ×2, spor tozu, şifa mantarı",
danger="Orta",
lore="Yürüyen dev bir mantar kümesi. Vurulunca zehirli spor bulutu patlatır; "
"yakın dövüşte nefes almak tehlikelidir.")),
("Gölge Kurdu (Alfa)", "Canavar (Elit)", "hostile",
desc(hp=40, dmg="10-16 (gölge pençesi)", atk="Yakın dövüş — hızlı, görünmez sıçrar",
dc=17, weak="ık, gümüş silah",
drops="Gölge postu ×1, alfa dişi, gümüş ore ×1",
danger="Yüksek",
lore="Karanlık ormanın en yaşlı kurt sürüsünün lideri. Gölgelerde erir, "
"arkandan belirir. Işık tuttuğunda bir an donar — tek fırsatın odur.")),
("Orman Haydutu", "Goblin", "hostile",
desc(hp=26, dmg="7-12 (paslı hançer)", atk="Yakın dövüş — pusu + hırsızlık",
dc=14, weak="Cephesel saldırı, gürültü",
drops="Çalıntı kese (5-20 gold), paslı hançer, kuru erzak",
danger="Orta",
lore="Yol kenarında pusu kuran sinsi bir goblin. Önce keseni çalmaya çalışır, "
"başaramazsa hançerine sarılır.")),
("Kanlı Yarasa", "Uçan Haşere", "hostile",
desc(hp=12, dmg="3-6 (kan emme)", atk="Havadan — dalış + kan emer",
dc=11, weak="Ateş, geniş savurma",
drops="Yarasa kanadı ×1-2, kan kesesi",
danger="Düşük",
lore="Mağara ağızlarında ve yaşlı ağaç kovuklarında asılı duran kan emici. "
"Sürü halinde daldığında kanını damla damla çeker.")),
("Bataklık Sülüğü", "Asalak", "hostile",
desc(hp=14, dmg="3-7 (yapışıp emer)", atk="Yakın dövüş — yapışır, sürekli hasar",
dc=12, weak="Tuz, ateş",
drops="Sülük özü, iyileştirici salgı ×1",
danger="Düşük-Orta",
lore="Ormanın nemli bataklık kıyısında saklanan dev sülük. Bacağına yapışır, "
"koparana dek yavaşça kan emer. Salgısı ilginç şekilde şifalıdır.")),
("Lanetli Karga Sürüsü", "Uçan Sürü", "hostile",
desc(hp=30, dmg="5-9 (gaga yağmuru)", atk="Alan — sürü halinde gözlere dalış",
dc=15, weak="Alan silahları, ateş, ses",
drops="Kara tüy ×3, lanetli boncuk, bazen gümüş ore",
danger="Orta-Yüksek",
lore="Tek bir kötü ruhu paylaşan bir karga sürüsü. Ayrı ayrı zayıf ama "
"birlikte gökten kara bir bulut gibi iner, gözleri hedef alır.")),
]
async def main():
dsn = settings.DATABASE_URL.replace("+asyncpg", "")
c = await asyncpg.connect(dsn)
forests = [r["id"] for r in await c.fetch(
"SELECT id FROM map_tags WHERE kind='region' AND category='forest' ORDER BY id"
)]
if not forests:
print("UYARI: forest region bulunamadı, map_tag_id NULL bırakılıyor.")
added = updated = 0
for i, (name, role, stance, description) in enumerate(ENEMIES):
tag = forests[i % len(forests)] if forests else None
existing = await c.fetchval("SELECT id FROM npcs WHERE name=$1", name)
if existing:
await c.execute(
"UPDATE npcs SET role=$2, stance=$3, description=$4, map_tag_id=$5, "
"updated_at=now() WHERE id=$1",
existing, role, stance, description, tag)
updated += 1
else:
await c.execute(
"INSERT INTO npcs(name,role,stance,description,map_tag_id) "
"VALUES($1,$2,$3,$4,$5)",
name, role, stance, description, tag)
added += 1
total = await c.fetchval("SELECT count(*) FROM npcs WHERE stance='hostile'")
print(f"Eklendi: {added}, güncellendi: {updated}. Toplam hostile NPC: {total}")
await c.close()
asyncio.run(main())

134
db/seed_equipment.py Normal file
View file

@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""
Stepstead Ekipman yuvaları + sınıf kullanım kısıtları + eksik eşyalar (2026-07-08).
Idempotent: tekrar çalıştırılabilir.
Yapar:
1. Caster silahı: 3 Asa (staff) + tarifleri (combat, plank+ingot).
2. Orphan balık fish_mullet'i Fırtına Kıyısı'na kaynak olarak ekler.
3. items.equip_slot + two_handed alanlarını kod önekinden doldurur.
4. item_classes tablosunu doldurur hangi sınıf hangi eşyayı KULLANABİLİR.
(Kayıt yoksa = tüm sınıflar kullanabilir: tool, mücevher, deri, tüketilebilir.)
Çalıştırma: /opt/stepstead/venv/bin/python3 /opt/stepstead/db/seed_equipment.py
"""
import asyncio
import json
import sys
import asyncpg
sys.path.insert(0, "/opt/stepstead")
from config import settings
DSN = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
# --- Sınıf id'leri (game_classes) ---
PALADIN, PRIEST, MAGE, HUNTER, WARRIOR, MONK, THIEF, WARLOCK = 2, 3, 4, 5, 6, 7, 8, 9
# --- Yeni caster silahı: Asa (staff) ---
# (code, ad, rarity, req_level, plank_family, cap_ingot, base_value)
STAVES = [
("staff_ash", "Dişbudak Asa", "rare", 20, "plank_ash", "ingot_silver", 130),
("staff_yew", "Porsuk Asa", "epic", 30, "plank_yew", "ingot_gold", 380),
("staff_ebony", "Abanoz Asa", "legendary", 40, "plank_ebony", "ingot_gold", 1250),
]
# --- Ekipman yuvası: kod öneki -> slot ---
SLOT_PREFIX = [
("sword_", "weapon"), ("dagger_", "weapon"), ("bow_", "weapon"), ("staff_", "weapon"),
("shield_", "shield"), ("armor_", "chest"),
("boots_", "feet"), ("gloves_", "hands"), ("belt_", "waist"), ("bag_", "back"),
("necklace_", "neck"), ("ring_", "finger"), ("bracelet_", "wrist"),
("axe_", "tool"), ("pickaxe_", "tool"), ("fishing_rod", "tool"),
]
TWO_HANDED_PREFIX = ("bow_", "staff_")
# --- Sınıf kullanım kısıtı: kod öneki -> izinli sınıf id listesi ---
# Liste boşsa (öneki burada yoksa) eşya herkese açıktır.
CLASS_PREFIX = [
("sword_", [PALADIN, WARRIOR]),
("shield_", [PALADIN, WARRIOR]),
("armor_", [PALADIN, WARRIOR]),
("bow_", [HUNTER]),
("dagger_", [PRIEST, MAGE, HUNTER, MONK, THIEF, WARLOCK]),
("staff_", [PRIEST, MAGE, WARLOCK]),
]
async def upsert_item(conn, code, name, itype, rarity, base_value, description):
return await conn.fetchval("""
INSERT INTO items(code,name,type,rarity,base_value,description)
VALUES($1,$2,$3,$4,$5,$6)
ON CONFLICT(code) DO UPDATE SET name=EXCLUDED.name, type=EXCLUDED.type,
rarity=EXCLUDED.rarity, base_value=EXCLUDED.base_value, description=EXCLUDED.description
RETURNING id
""", code, name, itype, rarity, base_value, description)
async def upsert_recipe(conn, code, name, out_id, skill, lvl, xp, steps, ings):
await conn.execute("""
INSERT INTO recipes(code,name,output_item_id,output_qty,skill,required_level,xp_reward,step_cost,ingredients)
VALUES($1,$2,$3,1,$4,$5,$6,$7,$8::jsonb)
ON CONFLICT(code) DO UPDATE SET name=EXCLUDED.name, output_item_id=EXCLUDED.output_item_id,
skill=EXCLUDED.skill, required_level=EXCLUDED.required_level, xp_reward=EXCLUDED.xp_reward,
step_cost=EXCLUDED.step_cost, ingredients=EXCLUDED.ingredients
""", code, name, out_id, skill, lvl, xp, steps, json.dumps(ings))
async def main():
conn = await asyncpg.connect(DSN)
# 1) Asalar + tarifleri
for code, name, rarity, lvl, plank, ingot, val in STAVES:
iid = await upsert_item(conn, code, name, "gear", rarity, val,
"Büyücü sınıflarının kullandığı çift el asa.")
await upsert_recipe(conn, f"craft_{code}", name, iid, "combat", lvl,
lvl * 4, 12,
[{"qty": 2, "item_code": plank}, {"qty": 1, "item_code": ingot},
{"qty": 1, "item_code": "vine"}])
# 2) Orphan balık -> Fırtına Kıyısı
kiyi = await conn.fetchval("SELECT id FROM locations WHERE code='firtina_kiyisi'")
if kiyi and await conn.fetchval("SELECT 1 FROM items WHERE code='fish_mullet'"):
await conn.execute("""
INSERT INTO location_resources(location_id,item_code,skill,step_cost,xp,cooldown_s)
VALUES($1,'fish_mullet','fishing',6,14,300)
ON CONFLICT(location_id,item_code) DO NOTHING
""", kiyi)
# 3) equip_slot + two_handed
items = await conn.fetch("SELECT id, code FROM items")
for r in items:
code = r["code"]
slot = None
for pref, s in SLOT_PREFIX:
if code.startswith(pref):
slot = s
break
two = any(code.startswith(p) for p in TWO_HANDED_PREFIX)
await conn.execute("UPDATE items SET equip_slot=$1, two_handed=$2 WHERE id=$3",
slot, two, r["id"])
# 4) item_classes — sınıf kullanım kısıtı
await conn.execute("TRUNCATE item_classes")
n = 0
for r in items:
code = r["code"]
for pref, class_ids in CLASS_PREFIX:
if code.startswith(pref):
for cid in class_ids:
await conn.execute(
"INSERT INTO item_classes(item_id,class_id) VALUES($1,$2) ON CONFLICT DO NOTHING",
r["id"], cid)
n += 1
break
slot_counts = await conn.fetch(
"SELECT COALESCE(equip_slot,'-') slot, count(*) c FROM items GROUP BY 1 ORDER BY 2 DESC")
await conn.close()
print(f"Asalar+tarifler eklendi ({len(STAVES)}). item_classes satır: {n}")
print("equip_slot dağılımı:", {r["slot"]: r["c"] for r in slot_counts})
asyncio.run(main())

152
db/seed_events_combat.py Normal file
View file

@ -0,0 +1,152 @@
"""
Lokasyona özel olaylar combat/hunting ırlıklı 8 yeni kart (2026-07-04).
events.location_codes: seyahatin VARIŞ lokasyonu listede ise olay çıkabilir;
NULL = her yerde. Çalıştırma (idempotent):
cd /opt/stepstead && ./venv/bin/python3 db/seed_events_combat.py
"""
import asyncio
import json
import sys
sys.path.insert(0, "/opt/stepstead")
import asyncpg # noqa: E402
from config import settings # noqa: E402
EVENTS = [
{
"code": "kurt_surusu",
"name": "Kurt Sürüsü",
"text": "Alacakaranlıkta parlayan gözler... Bir kurt sürüsü yolunu kesti. Liderleri dişlerini gösteriyor.",
"locations": ["av_topraklari", "karanlik_orman"],
"choices": [
{"text": "⚔ Savaş!", "skill": "combat", "dc": 13,
"success": {"text": "Sürüyü püskürttün, lider kurdu yere serdin.",
"items": [{"code": "meat_wolf", "qty": 2}, {"code": "leather_wolf", "qty": 1}],
"xp": {"skill": "combat", "amount": 60}},
"fail": {"text": "Kurtlar seni kovaladı, kan kaybederek kaçtın.", "steps": -150}},
{"text": "🏃 Kaç", "outcome": {"text": "Uzun bir koşuyla kurtuldun ama yorgun düştün.", "steps": -80}},
],
},
{
"code": "vahsi_domuz",
"name": "Vahşi Domuz",
"text": "Çalılıklar hışırdıyor — kocaman bir yaban domuzu toprağı eşeliyor. Henüz seni fark etmedi.",
"locations": ["av_topraklari", "fisilti_ormani", "akca_koru"],
"choices": [
{"text": "🏹 Sessizce avla", "skill": "hunting", "dc": 12,
"success": {"text": "Tek atışla indirdin. Temiz bir av.",
"items": [{"code": "meat_boar", "qty": 2}, {"code": "leather_boar", "qty": 1}],
"xp": {"skill": "hunting", "amount": 50}},
"fail": {"text": "Dal kırıldı, domuz sana saldırdı! Zor kaçtın.", "steps": -100}},
{"text": "🤫 Rahat bırak", "outcome": {"text": "Domuz homurdanarak uzaklaştı.", "steps": 0}},
],
},
{
"code": "geyik_izi",
"name": "Taze Geyik İzi",
"text": "Toprakta taze geyik izleri var. İzler sık çalılığın içine gidiyor.",
"locations": ["cayirlar", "av_topraklari", "gumus_camlik"],
"choices": [
{"text": "🦌 İzi sür", "skill": "hunting", "dc": 10,
"success": {"text": "İzi sürüp genç bir geyik avladın.",
"items": [{"code": "meat_deer", "qty": 1}, {"code": "leather_deer", "qty": 1}],
"xp": {"skill": "hunting", "amount": 40}},
"fail": {"text": "İzi çalılıkta kaybettin, boşa yürüdün.", "steps": -60}},
{"text": "➡ Yoluna devam et", "outcome": {"text": "Avı başka zamana bıraktın.", "steps": 0}},
],
},
{
"code": "magara_orumcegi",
"name": "Mağara Örümceği",
"text": "Madenin karanlık bir kovuğunda dev bir örümcek ağını germiş. Ağın arkasında parlayan bir cevher damarı var.",
"locations": ["derin_maden", "ejder_dagi", "tas_ocagi"],
"choices": [
{"text": "⚔ Örümceği öldür", "skill": "combat", "dc": 14,
"success": {"text": "Örümceği alt ettin, damardaki cevheri söktün.",
"items": [{"code": "ore_silver", "qty": 2}],
"xp": {"skill": "combat", "amount": 70}},
"fail": {"text": "Ağa dolandın! Kendini zor kurtardın.", "steps": -120}},
{"text": "🕯 Geri çekil", "outcome": {"text": "O gözlerle uğraşmaya değmez. Geri döndün.", "steps": -30}},
],
},
{
"code": "korsan_yagmasi",
"name": "Korsan Artığı",
"text": "Kıyıya vurmuş kırık bir filika... İçinde kilitli küçük bir para sandığı var. Ama iki serseri de sandığı görmüş, sana doğru geliyorlar.",
"locations": ["liman", "firtina_kiyisi"],
"choices": [
{"text": "⚔ Sandık için dövüş", "skill": "combat", "dc": 15,
"success": {"text": "Serserileri kaçırttın. Sandıktan gold çıktı!",
"gold": 40, "xp": {"skill": "combat", "amount": 80}},
"fail": {"text": "İki kişiye gücün yetmedi, dayak yiyip uzaklaştın.", "steps": -180}},
{"text": "🚶 Sandığı onlara bırak", "outcome": {"text": "Belaya bulaşmadın.", "steps": 0}},
],
},
{
"code": "yasli_avci",
"name": "Yaşlı Avcı",
"text": "Kamp ateşinin başında yaşlı bir avcı oturuyor. 'Otur evlat,' diyor, 'sana av hikâyeleri anlatayım.'",
"locations": ["av_topraklari", "cayirlar"],
"choices": [
{"text": "🔥 Otur ve dinle", "outcome": {"text": "Saatlerce iz sürme ve tuzak kurma taktikleri dinledin.",
"xp": {"skill": "hunting", "amount": 45}, "steps": -40}},
{"text": "🙏 Teşekkür edip geç", "outcome": {"text": "Vaktin yoktu, yola devam ettin.", "steps": 0}},
],
},
{
"code": "balikci_aginda",
"name": "Terk Edilmiş Ağ",
"text": "Kıyıda yarısı suda bir balııı duruyor. İçinde hâlâ çırpınan balıklar var ama ağ kayalara dolanmış.",
"locations": ["aynali_gol", "firtina_kiyisi", "liman"],
"choices": [
{"text": "🎣 Ağı çöz", "skill": "fishing", "dc": 11,
"success": {"text": "ı ustaca çözdün, balıklar senin.",
"items": [{"code": "fish_mackerel", "qty": 2}, {"code": "fish_bream", "qty": 1}],
"xp": {"skill": "fishing", "amount": 40}},
"fail": {"text": "Ağ iyice dolandı, üstüne bir de sırılsıklam oldun.", "steps": -70}},
{"text": "🌊 Denize bırak", "outcome": {"text": "Balıkları özgür bıraktın, için rahat.", "steps": 0}},
],
},
{
"code": "ayi_ini",
"name": "Ayı İni",
"text": "Kadim ormanın derinliklerinde bir in... İçeriden horultu geliyor. Girişte yarım kalmış bir bal peteği duruyor.",
"locations": ["kadim_orman", "karanlik_orman"],
"choices": [
{"text": "⚔ İne gir, ayıyla yüzleş", "skill": "combat", "dc": 16,
"success": {"text": "Efsanevi bir dövüştü. Ayıyı devirdin!",
"items": [{"code": "meat_bear", "qty": 2}, {"code": "leather_bear", "qty": 1}],
"xp": {"skill": "combat", "amount": 100}},
"fail": {"text": "Ayı seni köpek gibi kovaladı. Canını zor kurtardın.", "steps": -250}},
{"text": "🍯 Sadece peteği al ve kaç", "skill": "hunting", "dc": 12,
"success": {"text": "Sessizce peteği kapıp uzaklaştın.",
"items": [{"code": "herb_chamomile", "qty": 2}],
"xp": {"skill": "hunting", "amount": 30}},
"fail": {"text": "Ayı uyandı! Peteği bırakıp kaçtın.", "steps": -100}},
{"text": "🚶 Hiç bulaşma", "outcome": {"text": "Uyuyan ayıyı rahat bıraktın.", "steps": 0}},
],
},
]
async def main() -> None:
dsn = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
conn = await asyncpg.connect(dsn)
for ev in EVENTS:
await conn.execute("""
INSERT INTO events (code, name, text, choices, location_codes)
VALUES ($1, $2, $3, $4::jsonb, $5)
ON CONFLICT (code) DO UPDATE
SET name = EXCLUDED.name, text = EXCLUDED.text,
choices = EXCLUDED.choices, location_codes = EXCLUDED.location_codes
""", ev["code"], ev["name"], ev["text"], json.dumps(ev["choices"]), ev["locations"])
total = await conn.fetchval("SELECT count(*) FROM events")
targeted = await conn.fetchval("SELECT count(*) FROM events WHERE location_codes IS NOT NULL")
print(f"OK — toplam {total} olay ({targeted} lokasyona özel)")
await conn.close()
if __name__ == "__main__":
asyncio.run(main())

617
db/seed_quests.py Normal file
View file

@ -0,0 +1,617 @@
#!/usr/bin/env python3
"""
Stepstead Yan görev içeriği (2026-07-08).
WoW / Witcher tarzı ~90 yan görev + görev veren NPC'ler + yeni düşmanlar + görev itemleri.
Yapar (idempotent tekrar çalıştırılabilir):
1. Görev veren NPC'ler (friendly/neutral) → şehir/kasaba/tapınak/maden map_tag'lerine.
2. Yeni düşmanlar (hostile NPC) maden/tapınak/çorak/orman/çayır bölgelerine.
3. Görev itemleri items tablosu. Toplanabilenler location_resources'a (oyun lokasyonu).
Düşmandan düşenler item olarak eklenir (drop düşman ıklamasında yazılı).
4. ~90 yan görev (quests) + adım adım hedefler (quest_objectives).
Seed işareti: quests.author='Görev Tohumu', npcs (giver) description sonuna [seed:quest].
Çalıştırma: cd /opt/stepstead && ./venv/bin/python3 db/seed_quests.py
"""
import asyncio
import sys
sys.path.insert(0, "/opt/stepstead")
import asyncpg # noqa: E402
from config import settings # noqa: E402
SEED_TAG = "Görev Tohumu"
GIVER_MARK = "[seed:quest-giver]"
def edesc(hp, dmg, atk, dc, weak, drops, danger, lore):
return (f"{lore}\n\n⚔ Saldırı türü: {atk}\n❤ Can (HP): {hp}\n🗡 Hasar: {dmg}\n"
f"🎯 Savaş zorluğu (combat DC): {dc}\n💀 Tehlike: {danger}\n"
f"🩹 Zayıflık: {weak}\n🎁 Düşürdükleri: {drops}")
# ─────────────────────────── GÖREV VERENLER ───────────────────────────
# (key, ad, rol, yer_adı[map_tags.name], açıklama)
GIVERS = [
("doran", "Kaptan Doran", "Kale Muhafızı", "Aethelguard Citadel",
"Aethelguard Kalesi'nin sert ama adil muhafız kaptanı. Bölgedeki tehditleri maceracılara havale eder."),
("lysa", "Rahibe Lysa", "Şifacı Rahibe", "Aethelguard Citadel",
"Kale mabedinin şefkatli rahibesi. Hastalara şifalı ot ve kutsal su lazım olur."),
("aldwin", "Tüccar Aldwin", "Tüccar", "Aethelguard Citadel",
"Zengin ama korkak bir tüccar. Kervanları eşkıyalardan çekmediği için sürekli yardım arar."),
("verena", "Simyacı Verena", "Simyacı", "Aethelguard Citadel",
"Meraklı bir simyacı; deneyleri için nadir salgı, zehir ve mantar ister."),
("orin", "Kütüphaneci Orin", "Bilgin", "Aethelguard Citadel",
"Kadim metinlerin peşindeki yaşlı bilgin. Harabelerdeki tabletleri ve tomarları toplatır."),
("bramble", "Hancı Bramble", "Hancı", "Riftwood Crossing",
"Riftwood Geçidi hanının gürültücü sahibi. Müşterileri için et, balık ve dedikodu toplar."),
("kael", "Avcı Ustası Kael", "Avcı Ustası", "Riftwood Crossing",
"Bölgenin en iyi izcisi. Çırak yetiştirir; canavar avı ve deri siparişleri verir."),
("sefa", "Kervan Lideri Sefa", "Kervancı", "Riftwood Crossing",
"Kıtalar arası mal taşıyan cesur kervancı. Yol güvenliği ve teslimat işleri dağıtır."),
("grungni", "Ustabaşı Grungni", "Maden Ustabaşı", "Rivenhaven",
"Rivenhaven madeninin huysuz ustabaşı. Madeni istila eden yaratıklardan ve göçüklerden bıkmış."),
("nadia", "Kâhin Nadia", "Kâhin", "T-1",
"Terk edilmiş tapınağın son kâhini. Huzursuz ölüleri ve bozulan mühürleri dert eder."),
("thariel", "Baş Rahip Thariel", "Baş Rahip", "T-2",
"ık tarikatının baş rahibi. Lanetli sunakları arındıracak ve kutsal emanetleri bulacak eller arar."),
("bram", "Orman Bekçisi Bram", "Korucu", "Whisperwood",
"Whisperwood'un yalnız koruyucusu. Ormanın dengesini bozan her şeyi izler."),
("elindra", "Elf İzci Elindra", "İzci", "Luminara Grove",
"Luminara'nın zarif elf izcisi. Kutsal ağaçları ve nadir çiçekleri korur."),
("morvath", "Münzevi Morvath", "Münzevi Büyücü", "Xylos Prime",
"Ormanın derininde yaşayan tuhaf büyücü. Reagent'ları karşılığında sırlar fısıldar."),
("thessa", "Thessa Mor", "Görev Veren", "Rivenhaven",
"Gizemli görev dağıtıcısı; kimin için çalıştığı belirsiz, ama altını gerçek."),
("garrick", "Nalbant Garrick", "Nalbant", "Riftwood Crossing",
"Kervan atlarına bakan güçlü kuvvetli nalbant. Demir ve kömür sıkıntısı çeker."),
("wystan", "Yaşlı Wystan", "Emekli Şövalye", "Aethelguard Citadel",
"Bir zamanlar ünlü bir şövalye; artık dizleri tutmuyor, işlerini gençlere yaptırıyor."),
("seline", "Balıı Seline", "Balıı Reisi", "Riftwood Crossing",
"Kıyı balıılarının sözü geçen reisi. Fırtına, korsan ve dev balık derdi hiç bitmez."),
]
# ─────────────────────────── YENİ DÜŞMANLAR ───────────────────────────
# (ad, rol, bölge_adı[map_tags.name], desc)
ENEMIES = [
# Maden
("Kobold Madenci", "Kobold", "Rivenhaven",
edesc(20, "5-9 (kazma)", "Yakın dövüş — sürü", 13, "Ateş, dar geçitte kıstırma",
"Kobold kulağı ×1-2, ham bakır, mum", "Orta",
"Madenin karanlık dehlizlerini istila eden küçük, aç gözlü kobold. Parlak her şeyi çalar.")),
("Taş Golemi", "Golem", "Rivenhaven",
edesc(70, "14-20 (yumruk)", "Yakın dövüş — ağır", 17, "Kazma (mining), keskin köşeler",
"Golem çekirdeği ×1, kristal parçası ×2-3", "Yüksek",
"Maden derinliklerinde biriken mananın canlandırdığı taş yığını. Yavaş ama ezici.")),
("Kör Mağara Trolü", "Trol", "M-1",
edesc(90, "16-24 (sopa)", "Yakın dövüş — öfke", 18, "Ateş, ışık (kör eder)",
"Trol derisi ×1-2, kemik sopa, iri diş", "Çok Yüksek",
"Gözleri körelmiş dev bir mağara trolü. Sesi ve kokuyu takip eder; ateşten deliye döner.")),
("Derin Örümcek", "Dev Örümcek", "M-2",
edesc(28, "7-12 (zehir)", "Yakın dövüş — zehirli", 14, "Ateş, geniş alan",
"Örümcek zehir kesesi ×1-2, ipek", "Orta-Yüksek",
"Mağaranın dibinde ağ ören, at büyüklüğünde solgun örümcek. Zehri felç eder.")),
# Tapınak / undead
("Huzursuz İskelet", "Hortlak", "T-1",
edesc(24, "6-10 (paslı kılıç)", "Yakın dövüş", 13, "Kutsal su, kör (blunt) silah",
"İskelet kemiği ×1-2, paslı madeni para", "Orta",
"Mühür bozulunca uyanan eski muhafız iskeletleri. Kutsal su karşısında dağılır.")),
("Lanetli Rahip Hayaleti", "Hayalet (Elit)", "T-2",
edesc(60, "12-18 (mana lanet)", "Büyü — lanet + korku", 17, "Kutsal emanet, ışık büyüsü",
"Hayalet ektoplazması ×1-2, lanetli tomar", "Yüksek",
"Tapınağı ihanetle lekeleyen rahibin huzursuz ruhu. Sunak arındırılmadan yok edilemez.")),
("Taş Bekçi Heykeli", "Muhafız Golem", "T-2",
edesc(80, "15-22 (balyoz kol)", "Yakın dövüş — ağır", 18, "Kazma, çekirdeğini kırmak",
"Kutsal taş ×2, golem çekirdeği ×1", "Yüksek",
"Tapınağı bekleyen kadim heykel. Yabancıyı kutsal alanda görünce canlanır.")),
# Çorak (B-1 wasteland)
("Çorak Hortlağı", "Zombi", "B-1",
edesc(30, "7-11 (pençe)", "Yakın dövüş — hastalık", 14, "Ateş, kafa darbesi",
"Çürük et, kayıp madalyon (nadir)", "Orta",
"Çorak toprakta sürünen, hastalık saçan cesetler. Yavaş ama kalabalık gelir.")),
("Kum Solucanı", "Dev Solucan", "B-1",
edesc(75, "14-20 (yutma)", "Pusu — yer altından", 17, "Titreşimden kaçınmak, ateş",
"Kum solucanı dişi ×1-2, kristalize tuz", "Çok Yüksek",
"Çorak kumun altında bekleyen kör dev solucan. Ayak sesini duyunca patlarcasına fırlar.")),
("Leş Akbabası", "Uçan Yırtıcı", "B-1",
edesc(22, "6-9 (gaga)", "Havadan dalış", 13, "Ok/yay, yüksek zemin",
"Akbaba gagası, tüy demeti", "Orta",
"Çorakta leş kokusunu kilometrelerce öteden alan iri akbaba. Zayıfı seçer.")),
("Gölge İblisi", "İblis (Elit)", "B-1",
edesc(95, "18-26 (kara alev)", "Büyü — kara alev + korku", 19, "Kutsal emanet, ışık",
"İblis boynuzu ×1, kükürt, kara kristal", "Ölümcül",
"Yarıktan sızan alt-düzey iblis. Gölgeden gölgeye atlar, dokunduğunu yakar.")),
# Çayır / genel
("Yaban Domuzu Reisi", "Canavar (Elit)", "Solstice Mire",
edesc(50, "11-16 (azı dişi)", "Hücum — boynuzlama", 15, "Tuzak, mızrak",
"Domuz dişi ×1-2, kalın deri, yaban eti", "Orta-Yüksek",
"Sürünün yaşlı, yaralı ama azgın reisi. Hücumu ağaç deviren güçtedir.")),
("Dağ Ayısı", "Canavar (Elit)", "Emberfall",
edesc(65, "13-19 (pençe)", "Yakın dövüş — pençe+ısırık", 16, "Ateş, mesafe",
"Ayı pençesi ×1-2, kalın post, bal", "Yüksek",
"İninden erken uyanmış huysuz bir bozayı. Toprağını savunurken acımasızdır.")),
("Bataklık Harpisi", "Harpi", "Solstice Mire",
edesc(34, "8-13 (pençe dalış)", "Havadan taciz + çığlık", 15, "Ok/yay, ağ",
"Harpi tüyü ×1-2, çalınmış mücevher", "Orta-Yüksek",
"Bataklık üstünde çığlıklarla uçan yarı kuş yaratık. Parlak eşya için saldırır.")),
("Vahşi Şaman Goblin", "Goblin Büyücü", "Rivenwood",
edesc(40, "9-14 (şok totemi)", "Büyü — totem + şimşek", 15, "Totemini kırmak, hızlı baskın",
"Goblin kulağı ×2, totem parçası, garip iksir", "Orta-Yüksek",
"Goblin çetesini totemleriyle güçlendiren sinsi şaman. Önce totemi düşmeli.")),
("Gulyabani", "Yamyam Yaratık", "Xylos Prime",
edesc(45, "10-15 (tırnak)", "Pusu — karanlıktan", 16, "Ateş meşalesi, ışık",
"Solgun et, mezar tozu, kayıp yüzük", "Yüksek",
"Karanlık ormanda mezarları eşen soluk benizli yamyam. Işıktan tiksinir.")),
("Orman Şeytanı (Leshy)", "Bitki Ruhu (Elit)", "Ironbark Reach",
edesc(85, "15-22 (kök kırbaç)", "Yakın dövüş + köklerle bağlama", 18, "Ateş, gümüş balta",
"Kadim meşe özü ×2, canlı kök, uğur boncuğu", "Çok Yüksek",
"Ormanın öfkeli koruyucu ruhu. Witcher masalındaki gibi kurbanlarını köklerle sarar.")),
("Su Cini (Vodyanoy)", "Su Canavarı", "Aetherbloom",
edesc(48, "10-15 (pençe)", "Sudan pusu", 16, "Kuru zemine çekmek, ateş",
"Pullu deri, inci, sülük salgısı", "Orta-Yüksek",
"Gölet ve derelerde pusuya yatan pullu su cini. Balııları suya çeker.")),
]
# ─────────────────────────── GÖREV İTEMLERİ ───────────────────────────
# type: material (çoğu). gather_loc: oyun lokasyonu code'u (varsa location_resources'a eklenir), None=sadece drop.
# (code, ad, rarity, gather_loc, skill, step, xp, açıklama)
QITEMS = [
# Toplanabilir reagent'lar
("herb_healroot", "Şifalı Kök", "common", "fisilti_ormani", "foraging", 5, 9, "Rahibe Lysa'nın merhemleri için kök."),
("herb_moonflower", "Ay Çiçeği", "uncommon", "cayirlar", "foraging", 6, 12, "Sadece dolunayda açan soluk mavi çiçek."),
("herb_bluecap", "Mavi Mantar", "common", "kadim_orman", "foraging", 5, 9, "Nemli kütüklerde biten mavi şapkalı mantar."),
("mat_wildhoney", "Yaban Balı", "uncommon", "akca_koru", "foraging", 7, 13, "Vahşi arı kovanından çıkarılan koyu bal."),
("mat_crystal", "Kristal Parçası", "uncommon", "derin_maden", "mining", 7, 13, "Maden derinliğinde parıldayan ham kristal."),
("mat_ironslag", "Demir Cürufu", "common", "tas_ocagi", "mining", 5, 9, "Eritme artığı; nalbantın işine yarar."),
("mat_pearlshell", "İnci Kabuğu", "uncommon", "aynali_gol", "fishing", 6, 12, "İçinde bazen inci taşıyan sedefli kabuk."),
("mat_holywater", "Kutsal Su", "rare", "firtina_kiyisi", "fishing", 8, 16, "Kutsanmış kaynaktan alınan berrak su."),
("mat_ancientoak", "Kadim Meşe Özü", "rare", "kadim_orman", "woodcutting", 8, 16, "Yüzyıllık meşenin sızan özü; büyüde güçlüdür."),
("mat_salt", "Kristalize Tuz", "common", "firtina_kiyisi", "foraging", 5, 9, "Kıyı kayalarında biriken tuz kristali."),
# Sadece drop / bulunacak eşyalar
("q_wolf_fang", "Kurt Dişi", "common", None, None, 0, 0, "Kurt sürülerinden toplanan sivri diş."),
("q_spider_venom", "Örümcek Zehir Kesesi", "uncommon", None, None, 0, 0, "Dev örümceklerden alınan zehir kesesi."),
("q_kobold_ear", "Kobold Kulağı", "common", None, None, 0, 0, "Kobold avının kanıtı."),
("q_skeleton_bone", "İskelet Kemiği", "common", None, None, 0, 0, "Huzursuz ölülerden kalan kemik."),
("q_golem_core", "Golem Çekirdeği", "rare", None, None, 0, 0, "Golemi çalıştıran manalı taş çekirdek."),
("q_ectoplasm", "Hayalet Ektoplazması", "rare", None, None, 0, 0, "Hayaletten sızan titreşen madde."),
("q_harpy_feather", "Harpi Tüyü", "uncommon", None, None, 0, 0, "Bataklık harpisinin keskin tüyü."),
("q_bear_claw", "Ayı Pençesi", "uncommon", None, None, 0, 0, "Dağ ayısının kavisli pençesi."),
("q_goblin_ear", "Goblin Kulağı", "common", None, None, 0, 0, "Goblin çetesi avının kanıtı."),
("q_troll_hide", "Trol Derisi", "rare", None, None, 0, 0, "Kalın, ok geçirmez trol derisi."),
("q_demon_horn", "İblis Boynuzu", "epic", None, None, 0, 0, "Gölge iblisinden koparılan kara boynuz."),
("q_worm_tooth", "Kum Solucanı Dişi", "rare", None, None, 0, 0, "Kum solucanının testere gibi dişi."),
("q_boar_tusk", "Domuz Dişi", "common", None, None, 0, 0, "Yaban domuzu reisinin azı dişi."),
("q_leech_slime", "Sülük Salgısı", "common", None, None, 0, 0, "Bataklık sülüğünden alınan yapışkan salgı."),
("q_medallion", "Kayıp Madalyon", "rare", None, None, 0, 0, "Bir soylunun kayıp aile madalyonu."),
("q_ancient_tablet","Kadim Tablet", "rare", None, None, 0, 0, "Eski bir dille yazılı taş tablet."),
("q_cursed_scroll", "Lanetli Tomar", "rare", None, None, 0, 0, "Karanlık büyü içeren mühürlü tomar."),
("q_holy_relic", "Kutsal Emanet", "epic", None, None, 0, 0, "ık tarikatının kayıp kutsal emaneti."),
("q_lost_ring", "Kayıp Yüzük", "uncommon", None, None, 0, 0, "Bir maceracının mezardan çıkan yüzüğü."),
("q_totem_shard", "Totem Parçası", "common", None, None, 0, 0, "Goblin şamanın kırık totemi."),
]
# ─────────────────────────── YAN GÖREVLER ───────────────────────────
# (başlık, giver_key, bölge_adı[map_tags.name] veya None, özet, [hedef notları])
QUESTS = [
# ── Kaptan Doran — bounty / güvenlik ──
("Kurt Belası", "doran", "Veridian Glade",
"Aç kurtlar kale yollarına inmeye başladı. Doran onları seyrekleştirmeni istiyor.",
["Veridian Glade'de 6 Aç Kurt öldür.", "Kanıt olarak 4 Kurt Dişi topla.", "Kaptan Doran'a dön."]),
("Örümcek Yuvası", "doran", "Luminara Grove",
"Zehirli örümcekler bir tüccarı zehirledi. Yuvayı temizle.",
["Luminara Grove'da 5 Zehirli Orman Örümceği öldür.", "3 Örümcek Zehir Kesesi getir."]),
("Haydut Tehdidi", "doran", "Solstice Mire",
"Orman haydutları kervan yolunu kesiyor. Reisleri bir ders almalı.",
["Solstice Mire'da 5 Orman Haydutu öldür.", "Çaldıkları keseyi geri al.", "Doran'a bildir."]),
("Gölge Kurdu Avı", "doran", "Emberfall",
"Bir alfa kurt sürüsü kontrolden çıkardı. Elit hedefi indir.",
["Emberfall'da Gölge Kurdu (Alfa)'yı öldür.", "Postunu kanıt getir."]),
("Kanatlı Bela", "doran", "Xylos Prime",
"Kanlı yarasalar geceleri nöbetçilere saldırıyor.",
["Xylos Prime'da 8 Kanlı Yarasa öldür.", "5 Yarasa Kanadı topla."]),
("Karga Laneti", "doran", "Veridian Glade",
"Lanetli bir karga sürüsü tarlaları talan ediyor; uğursuzluk saçıyorlar.",
["Lanetli Karga Sürüsü'nü dağıt.", "Lanetli boncuğu Rahibe Lysa'ya götür."]),
# ── Rahibe Lysa — şifa / topla ──
("Merhem İçin Kök", "lysa", "Fısıltı Ormanı",
"Revirdeki yaralılar için şifalı kök tükendi.",
["Fısıltı Ormanı'nda 8 Şifalı Kök topla.", "Rahibe Lysa'ya teslim et."]),
("Dolunay Çiçeği", "lysa", None,
"Ateşi düşmeyen bir çocuk için nadir ay çiçeği gerek.",
["Uçsuz Çayırlar'da 6 Ay Çiçeği topla.", "Lysa'ya getir."]),
("Kutsal Su Seferi", "lysa", None,
"Vaftiz ayini için kutsanmış kaynak suyu lazım.",
["Fırtına Kıyısı'ndan 4 Kutsal Su al.", "Mabede götür."]),
("Salgın Durdurulmalı", "lysa", "B-1",
"Çorak topraktan gelen hortlaklar hastalık yayıyor; kaynağı azalt.",
["Çorak Hortlağı'ndan 6 öldür.", "5 Mavi Mantar topla (panzehir için).", "Lysa'ya dön."]),
("Arı Kovanı Riski", "lysa", None,
"Yaralı yanıkları için yaban balı gerekiyor ama kovanlar tehlikeli.",
["Akça Koru'dan 5 Yaban Balı topla.", "Lysa'ya teslim et."]),
# ── Tüccar Aldwin — teslimat / eskort ──
("Kervan Koruması", "aldwin", "Solstice Mire",
"Aldwin'in kervanı bataklıktan geçerken haydut korkusu içinde.",
["Kervan yolundaki 4 Orman Haydutu'nu temizle.", "Aldwin'e güvenli yolu bildir."]),
("Kayıp Sevkiyat", "aldwin", "Rivenwood",
"Bir yük arabası devrildi, mallar ormana saçıldı.",
["Rivenwood'da 5 kayıp sandığı bul (araştır).", "Goblinlerin çaldığını Aldwin'e söyle."]),
("Değerli Teslimat", "aldwin", "Riftwood Crossing",
"Kırılgan bir sipariş Riftwood'a ulaştırılmalı.",
["Paketi Hancı Bramble'a götür.", "İmzalı makbuzu Aldwin'e geri getir."]),
("Borçlu Kaçtı", "aldwin", "Karaağaç Karanlığı",
"Aldwin'e borçlu bir adam karanlık ormana kaçmış.",
["Karaağaç Karanlığı'nda borçluyu bul.", "Ya altını ya sözünü Aldwin'e getir."]),
("Rakip Casusu", "aldwin", "Aethelguard Citadel",
"Aldwin rakip bir tüccarın kendisini gözetlediğinden şüpheleniyor.",
["Kalede casusu takip et (araştır).", "Bulguları Aldwin'e ilet."]),
# ── Simyacı Verena — reagent ──
("Zehir Deneyi", "verena", "Luminara Grove",
"Verena panzehir formülü için taze örümcek zehri istiyor.",
["4 Örümcek Zehir Kesesi topla.", "Verena'ya getir."]),
("Mantar Koleksiyonu", "verena", "Kadim Orman",
"Nadir mavi mantarlar bir iksirin anahtarı.",
["Kadim Orman'da 10 Mavi Mantar topla.", "Verena'nın laboratuvarına bırak."]),
("Kristalin Sırrı", "verena", "Rivenhaven",
"Verena maden kristalinin manasını incelemek istiyor.",
["Derin Maden'den 6 Kristal Parçası çıkar.", "Verena'ya teslim et."]),
("Ektoplazma Örneği", "verena", "T-2",
"Hayalet maddesi Verena'nın en riskli deneyi için gerekli.",
["Lanetli Rahip Hayaleti'nden 2 Hayalet Ektoplazması al.", "Kırılmadan Verena'ya götür."]),
("Sülük Salgısı", "verena", "Slyvandor",
"Yapışkan sülük salgısı yeni bir yara bandı denemesi için.",
["Bataklık Sülüğü'nden 5 Sülük Salgısı topla.", "Verena'ya ver."]),
("İblis Kükürdü", "verena", "B-1",
"Verena tehlikeli bir simya için iblis boynuzu tozu istiyor — delilik mi cesaret mi?",
["Gölge İblisi'nden 1 İblis Boynuzu al.", "Verena'ya teslim et (dikkatli!)."]),
# ── Kütüphaneci Orin — araştır / kadim ──
("Kayıp Tabletler", "orin", "M-1",
"Orin mağara duvarlarındaki kadim yazıtları istiyor.",
["M-1 mağarasında 3 Kadim Tablet bul.", "Orin'e getir."]),
("Yasak Tomar", "orin", "T-1",
"Tapınak arşivinde lanetli bir tomar saklı; Orin onu güvenle incelemek istiyor.",
["Huzursuz İskeletleri geçip 1 Lanetli Tomar al.", "Orin'e teslim et."]),
("Harabelerin Sırrı", "orin", "Rivenhaven",
"Madende eski bir uygarlığın izleri var.",
["Rivenhaven'da 4 oyma taş incele (araştır).", "Çizimleri Orin'e ver."]),
("Kadim Meşenin Bilgeliği", "orin", "Kadim Orman",
"Orin kadim meşe özünün büyülü kaydını çözmek istiyor.",
["3 Kadim Meşe Özü topla.", "Orin'in masasına bırak."]),
("Kayıp Günlük", "orin", "Whisperwood",
"Kaybolan bir kaşifin günlüğü ormanda bir yerlerde.",
["Whisperwood'da kaşifin kampını bul (araştır).", "Günlüğü Orin'e getir."]),
# ── Hancı Bramble — yemek / dedikodu ──
("Han Mutfağı: Et", "bramble", "Av Toprakları",
"Han için taze yaban eti lazım.",
["Av Toprakları'nda avlan, 8 yaban eti getir.", "Bramble'a teslim et."]),
("Han Mutfağı: Balık", "bramble", None,
"Cuma ziyafeti için taze balık gerek.",
["Aynalı Göl'de 10 balık tut.", "Bramble'a getir."]),
("Bal Likörü", "bramble", "Akça Koru",
"Bramble'ın meşhur bal likörü için yaban balı bitti.",
["6 Yaban Balı topla.", "Bramble'a ver."]),
("Fıçı Baskını", "bramble", "Rivenwood",
"Goblinler han fıçılarını çaldı!",
["Rivenwood'da 4 Orman Haydutu/Goblin öldür.", "Çalınan fıçıyı geri getir."]),
("Dedikodu Toplayıcı", "bramble", "Riftwood Crossing",
"Bramble kasabadaki söylentileri merak ediyor.",
["3 kasabalıyla konuş (araştır).", "Duyduklarını Bramble'a anlat."]),
# ── Avcı Ustası Kael — av / deri ──
("Çırak Sınavı: Tavşan", "kael", "Av Toprakları",
"Kael'in çırağı olmak için önce mütevazı bir avla başla.",
["Av Toprakları'nda 6 tavşan avla.", "6 tavşan derisi getir."]),
("Reis Domuz", "kael", "Solstice Mire",
"Yaban domuzu reisi bir avcıyı yaraladı; onu indir.",
["Yaban Domuzu Reisi'ni öldür.", "2 Domuz Dişi getir."]),
("Ayı Sorunu", "kael", "Emberfall",
"Erken uyanan bir dağ ayısı avlanma alanını basıyor.",
["Dağ Ayısı'nı öldür.", "Ayı Pençesi ve postu Kael'e getir."]),
("Kaliteli Deri Siparişi", "kael", "Av Toprakları",
"Bir zırhçı için kalın deri gerekiyor.",
["Geyik avla, 8 geyik derisi topla.", "Kael'e teslim et."]),
("Harpi Tehdidi", "kael", "Solstice Mire",
"Bataklık harpileri avcıların tuzaklarını yağmalıyor.",
["3 Bataklık Harpisi öldür.", "4 Harpi Tüyü getir."]),
("Kaçak Yavru", "kael", "Whisperwood",
"Nadir bir geyik yavrusu sürüden ayrı düşmüş; kurtlardan önce bul.",
["Whisperwood'da yavruyu bul (araştır).", "Güvenle Kael'e getir."]),
# ── Kervan Lideri Sefa — yol / teslimat ──
("Yol Temizliği", "sefa", "Ironbark Reach",
"Sefa'nın rotasını sarmaşık cinleri kapatmış.",
["Ironbark Reach'te 4 Sarmaşık Cini öldür.", "Yolu Sefa'ya rapor et."]),
("Kıtalar Arası Paket", "sefa", "Riftwood Crossing",
"Meridia'ya gidecek hassas bir paket koruma istiyor.",
["Paketi limana kadar koru (eskort).", "Sefa'ya teslim onayını getir."]),
("Kayıp Kervancı", "sefa", "Karaağaç Karanlığı",
"Sefa'nın bir adamı karanlık ormanda kayıp.",
["Karaağaç Karanlığı'nda kervancıyı ara (araştır).", "Gulyabani tehdidini temizle.", "Adamı geri getir."]),
("Nal Sevkiyatı", "sefa", "Riftwood Crossing",
"Nalbant Garrick'e demir cürufu ulaştırılmalı.",
["Taş Ocağı'ndan 8 Demir Cürufu topla.", "Garrick'e teslim et."]),
("Korsan Uyarısı", "sefa", None,
"Kıyıda korsan görüldü; Sefa deniz rotasını merak ediyor.",
["Fırtına Kıyısı'nı gözle (araştır).", "Seline ile konuş.", "Sefa'ya bildir."]),
# ── Ustabaşı Grungni — maden ──
("Kobold İstilası", "grungni", "Rivenhaven",
"Kobold sürüsü madenin alt katını ele geçirdi.",
["Rivenhaven'da 8 Kobold Madenci öldür.", "6 Kobold Kulağı getir."]),
("Taştan Bela", "grungni", "Rivenhaven",
"Bir taş golemi maden geçidini tıkadı.",
["Taş Golemi'ni parçala.", "Golem Çekirdeği'ni Grungni'ye getir."]),
("Göçük Kurtarması", "grungni", "Rivenhaven",
"Göçük altında kalan madenciler var.",
["3 madenciyi kazıp çıkar (araştır).", "Güvenli çıkışı Grungni'ye bildir."]),
("Kristal Kotası", "grungni", "Derin Maden",
"Kalenin siparişi için kristal çıkarılmalı.",
["Derin Maden'den 10 Kristal Parçası çıkar.", "Grungni'ye teslim et."]),
("Derindeki Örümcek", "grungni", "M-2",
"En alt galeride dev bir örümcek yuva kurmuş.",
["Derin Örümcek'i öldür.", "Yuvayı yak.", "Grungni'ye haber ver."]),
("Kör Trol", "grungni", "M-1",
"Kör bir mağara trolü madencileri dehşete düşürüyor.",
["Kör Mağara Trolü'nü indir.", "Trol Derisi'ni kanıt getir."]),
# ── Kâhin Nadia — tapınak/undead ──
("Huzursuz Ölüler", "nadia", "T-1",
"Tapınak mührü zayıfladı, iskeletler uyandı.",
["T-1'de 8 Huzursuz İskelet dağıt.", "6 İskelet Kemiği topla."]),
("Mührü Onar", "nadia", "T-1",
"Kırık mühür yeniden çizilmeli.",
["3 Kutsal Su getir.", "Sunakta ritüeli tamamla.", "Nadia'ya bildir."]),
("Kayıp Kâhin", "nadia", "M-1",
"Nadia'nın çırağı kehanet ararken mağarada kaybolmuş.",
["M-1'de çırağı bul (araştır).", "Sağ salim Nadia'ya getir."]),
("Kehanet Tableti", "nadia", "M-2",
"Bir kehanet kadim bir tablete kazınmış.",
["M-2'de 2 Kadim Tablet bul.", "Nadia'ya oku."]),
# ── Baş Rahip Thariel — kutsal ──
("Lanetli Sunak", "thariel", "T-2",
"İhanetle lekelenen sunak arındırılmalı.",
["Lanetli Rahip Hayaleti'ni yok et.", "Sunağı kutsal su ile arındır.", "Thariel'e dön."]),
("Kutsal Emanet", "thariel", "T-2",
"Tarikatın kutsal emaneti yağmalanmış, izini sür.",
["Taş Bekçi Heykeli'ni geç.", "1 Kutsal Emanet'i geri al.", "Thariel'e teslim et."]),
("Karanlığı Kov", "thariel", "B-1",
"Çorak topraktan yayılan karanlık tapınağı tehdit ediyor.",
["B-1'de 1 Gölge İblisi'ni kov.", "İblis Boynuzu'nu mühürle.", "Thariel'e getir."]),
("Kutsal Su Kuyusu", "thariel", None,
"Mabet kuyusu kurudu; kutsanacak temiz su gerek.",
["Fırtına Kıyısı'ndan 6 Kutsal Su getir.", "Thariel kutsayacak."]),
("Hac Yolu Güvenliği", "thariel", "T-1",
"Hacılar huzursuz ölülerden korkuyor.",
["T-1 yolundaki 6 Huzursuz İskelet'i temizle.", "Thariel'e güvenli yolu bildir."]),
# ── Orman Bekçisi Bram — orman dengesi ──
("Ormanın Dengesi", "bram", "Whisperwood",
"Yaşlı Ağaç Adam öfkelenip yolcuları eziyor; sakinleştir ya da indir.",
["Whisperwood'da Yaşlı Ağaç Adam'ı yatıştır ya da öldür.", "Bram'a bildir."]),
("Spor Salgını", "bram", "Rivenwood",
"Mantar spor yaratıkları ormanı zehirliyor.",
["Rivenwood'da 6 Mantar Spor Yaratığı öldür.", "Zehirli sporları yak."]),
("Kaçak Ateş", "bram", "Emberfall",
"Emberfall'da kontrolsüz bir yangın çıkabilir; kaynağı bul.",
["Emberfall'da yangın izini araştır.", "Bram'a raporla."]),
("Leshy Uyanışı", "bram", "Ironbark Reach",
"Bir orman şeytanı (Leshy) uyanmış; Witcher misali onu alt et.",
["Ironbark Reach'te Orman Şeytanı (Leshy)'yi öldür.", "Kadim Meşe Özü'nü Bram'a getir."]),
("Fare İstilası", "bram", "Aetherbloom",
"Dev orman fareleri ağaç köklerini kemiriyor.",
["Aetherbloom'da 8 Dev Orman Faresi öldür.", "Bram'a bildir."]),
# ── Elf İzci Elindra — nadir doğa ──
("Ay Çiçeği Kutsaması", "elindra", None,
"Elindra'nın ritüeli için el değmemiş ay çiçekleri gerek.",
["Uçsuz Çayırlar'da 8 Ay Çiçeği topla.", "Elindra'ya getir."]),
("Kutsal Ağaç Bekçisi", "elindra", "Luminara Grove",
"Örümcekler kutsal ağaca ağ örüyor.",
["Luminara Grove'da 6 Zehirli Orman Örümceği öldür.", "Ağacı temizle."]),
("İnci Gözyaşı", "elindra", None,
"Elindra'nın kolyesi için berrak bir göl incisi gerek.",
["Aynalı Göl'de 5 İnci Kabuğu topla.", "Elindra'ya ver."]),
("Su Cini Tehdidi", "elindra", "Aetherbloom",
"Bir su cini derenin balıklarını ve balııları yutuyor.",
["Aetherbloom'da Su Cini (Vodyanoy)'u öldür.", "Elindra'ya haber ver."]),
# ── Münzevi Morvath — gizemli reagent ──
("Reagent Toplayıcı", "morvath", "Xylos Prime",
"Morvath büyüsü için üç farklı reagent istiyor.",
["4 Kadim Meşe Özü, 4 Mavi Mantar, 4 Kristal Parçası getir.", "Morvath'a ver."]),
("Kara Tomarın Peşinde", "morvath", "T-1",
"Morvath yasak bilgi için lanetli bir tomar istiyor.",
["T-1'den 1 Lanetli Tomar al.", "Morvath'a gizlice getir."]),
("Gölge Özü", "morvath", "B-1",
"Morvath karanlık bir deney için iblis maddesi arıyor.",
["Gölge İblisi'nden 1 İblis Boynuzu al.", "Morvath'a teslim et."]),
("Totem Kırıcı", "morvath", "Rivenwood",
"Vahşi şaman goblin Morvath'ın çizdiği sınırı bozuyor.",
["Vahşi Şaman Goblin'i öldür.", "2 Totem Parçası getir."]),
# ── Thessa Mor — gizemli / karışık ──
("Kayıp Madalyon", "thessa", "B-1",
"Thessa bir soylunun kayıp madalyonunu istiyor; nedenini söylemiyor.",
["Çorak Hortlağı'ndan 1 Kayıp Madalyon düşür.", "Thessa'ya getir."]),
("Mezar Yüzüğü", "thessa", "Karaağaç Karanlığı",
"Bir maceracının yüzüğü gulyabaninin ininde.",
["Gulyabani'yi öldür.", "1 Kayıp Yüzük al.", "Thessa'ya teslim et."]),
("İsimsiz İş", "thessa", "M-2",
"Thessa mağaranın dibinden bir şey getirmeni istiyor — soru sorma.",
["M-2'de gizli sandığı bul (araştır).", "Açmadan Thessa'ya getir."]),
("Sessiz Tanık", "thessa", "Rivenhaven",
"Bir olaya tanık olan madenci ortadan kaybolmuş.",
["Rivenhaven'da tanığı ara (araştır).", "Bulguları Thessa'ya fısılda."]),
("Kum Altındaki Sır", "thessa", "B-1",
"Çorakta bir şey gömülü; kum solucanı üstünde bekliyor.",
["Kum Solucanı'nı öldür.", "1 Kum Solucanı Dişi ve gömülü kutuyu getir."]),
# ── Nalbant Garrick — demir/kömür ──
("Demir Cürufu Lazım", "garrick", "Taş Ocağı",
"Garrick'in nal ocağı için cüruf tükendi.",
["Taş Ocağı'ndan 10 Demir Cürufu topla.", "Garrick'e getir."]),
("At Nalı Siparişi", "garrick", "Riftwood Crossing",
"Kervan atları için toplu nal gerek; malzeme topla.",
["6 Demir Cürufu ve 3 bakır külçe getir.", "Garrick'e teslim et."]),
("Kayıp Örs", "garrick", "Rivenwood",
"Garrick'in kıymetli örsü çalınmış — goblin işi.",
["Rivenwood'da goblin kampını bul (araştır).", "Örsü geri getir."]),
# ── Yaşlı Wystan — nostalji / bounty ──
("Eski Kılıcım", "wystan", "M-1",
"Wystan gençliğinin kılıcını bir mağarada bırakmış.",
["M-1'de eski kılıcı bul (araştır).", "Wystan'a getir."]),
("Son Bir Av", "wystan", "Emberfall",
"Wystan bir kez daha büyük bir av görmek istiyor; onun adına indir.",
["Dağ Ayısı'nı öldür.", "Postu Wystan'a göster."]),
("Şövalyenin Onuru", "wystan", "B-1",
"Wystan'ı yenen iblis hâlâ çorakta; intikamını al.",
["Gölge İblisi'ni öldür.", "İblis Boynuzu'nu Wystan'a getir."]),
("Zırhımı Onar", "wystan", "Taş Ocağı",
"Wystan'ın eski zırhı için metal gerek.",
["Taş Ocağı'ndan 8 Demir Cürufu topla.", "Bir demirciye onartıp Wystan'a getir."]),
# ── Balıı Seline — deniz/göl ──
("Fırtına Avı", "seline", None,
"Fırtına öncesi balık sürüsü kıyıya yaklaştı.",
["Fırtına Kıyısı'nda 12 balık tut.", "Seline'ye getir."]),
("İnci Dalışı", "seline", None,
"Seline'nin borcu için değerli inci gerek.",
["Aynalı Göl'de 8 İnci Kabuğu topla.", "Seline'ye ver."]),
("Dev Balık Efsanesi", "seline", None,
"Gölde efsanevi bir balık görülmüş; kanıt getir.",
["Aynalı Göl'ü araştır.", "Efsane balığı yakala.", "Seline'ye göster."]),
("Korsan Kampı", "seline", "B-1",
"Kıyı korsanları balıı ağlarını yağmalıyor.",
["Kıyıdaki 4 haydudu/korsanı temizle.", "Seline'ye bildir."]),
("Sülük Bela", "seline", "Slyvandor",
"Bataklık sülükleri balık ağlarına doluşuyor.",
["Slyvandor'da 6 Bataklık Sülüğü öldür.", "5 Sülük Salgısı Verena için topla."]),
# ── Ek karışık (dünya doldurma) ──
("Tuz Toplayıcı", "bramble", None,
"Han'ın eti kürlemek için tuza ihtiyacı var.",
["Fırtına Kıyısı'ndan 8 Kristalize Tuz topla.", "Bramble'a getir."]),
("Akbaba Sürüsü", "doran", "B-1",
"Leş akbabaları çorak yolcularına saldırıyor.",
["B-1'de 6 Leş Akbabası öldür.", "Doran'a bildir."]),
("Kutsal Taş", "thariel", "T-2",
"Sunak onarımı için bekçi heykelinden kutsal taş gerek.",
["Taş Bekçi Heykeli'ni yık.", "2 Kutsal Taş getir."]),
("Bal ve Mum", "grungni", "Akça Koru",
"Madenciler karanlıkta çalışmak için mum, ustabaşı da tatlı istiyor.",
["Akça Koru'dan 5 Yaban Balı topla.", "Grungni'ye getir."]),
("Goblin Reisi", "kael", "Rivenwood",
"Goblin çetesinin şamanı bölgeyi terörize ediyor.",
["Vahşi Şaman Goblin'i öldür.", "4 Goblin Kulağı getir."]),
("Kristalize Tuz Siparişi", "verena", None,
"Verena koruyucu tuz çemberi için saf tuz istiyor.",
["Fırtına Kıyısı'ndan 10 Kristalize Tuz topla.", "Verena'ya ver."]),
("Yolcu Kurtarma", "sefa", "Solstice Mire",
"Bataklıkta mahsur kalan bir yolcu yardım bekliyor.",
["Solstice Mire'da yolcuyu bul (araştır).", "Harpilerden koru.", "Sefa'ya getir."]),
("Kadim Kapı", "orin", "M-2",
"Orin mağaranın dibindeki mühürlü kapıyı çözmek istiyor.",
["M-2'de kapının 3 sembolünü incele (araştır).", "Çizimi Orin'e getir."]),
("Şifalı Çorba", "lysa", "Av Toprakları",
"Hasta çocuklar için besleyici et suyu gerek.",
["Av Toprakları'nda 6 yaban eti getir.", "Lysa'ya teslim et."]),
("Kayıp Çoban", "bram", "Uçsuz Çayırlar mı?",
"Çayırlarda bir çoban ve sürüsü kaybolmuş.",
["Uçsuz Çayırlar'ı araştır.", "Çobanı bul.", "Bram'a bildir."]),
("Örümcek İpeği", "verena", "M-2",
"Verena dayanıklı örümcek ipeği ile deney yapacak.",
["Derin Örümcek'ten 4 ipek topla.", "Verena'ya getir."]),
("Kayıp Madenci Ailesi", "grungni", "Rivenhaven",
"Bir madencinin ailesi haber bekliyor; adamı bul.",
["Rivenhaven'da kayıp madenciyi ara (araştır).", "Haberi Grungni'ye ilet."]),
("Tapınak Çanı", "nadia", "T-2",
"Tapınak çanı düşmüş; ruhları uzak tutan sesi geri getir.",
["T-2'de düşen çanı bul (araştır).", "Bekçiyi geçip çanı as.", "Nadia'ya bildir."]),
]
async def resolve_map_tag(c, name):
if not name or name.endswith("mı?"):
return None
return await c.fetchval("SELECT id FROM map_tags WHERE name=$1", name)
async def main():
dsn = settings.DATABASE_URL.replace("+asyncpg", "")
c = await asyncpg.connect(dsn)
# 1) Görev verenler (upsert by name)
giver_ids = {}
for key, name, role, place, about in GIVERS:
tag = await c.fetchval("SELECT id FROM map_tags WHERE name=$1", place)
desc = f"{about}\n\n{GIVER_MARK}"
gid = await c.fetchval("SELECT id FROM npcs WHERE name=$1", name)
if gid:
await c.execute("UPDATE npcs SET role=$2,stance='friendly',description=$3,"
"map_tag_id=$4,updated_at=now() WHERE id=$1", gid, role, desc, tag)
else:
gid = await c.fetchval("INSERT INTO npcs(name,role,stance,description,map_tag_id) "
"VALUES($1,$2,'friendly',$3,$4) RETURNING id", name, role, desc, tag)
giver_ids[key] = gid
# 2) Düşmanlar (upsert by name)
for name, role, region, desc in ENEMIES:
tag = await c.fetchval("SELECT id FROM map_tags WHERE name=$1", region)
eid = await c.fetchval("SELECT id FROM npcs WHERE name=$1", name)
if eid:
await c.execute("UPDATE npcs SET role=$2,stance='hostile',description=$3,"
"map_tag_id=$4,updated_at=now() WHERE id=$1", eid, role, desc, tag)
else:
await c.execute("INSERT INTO npcs(name,role,stance,description,map_tag_id) "
"VALUES($1,$2,'hostile',$3,$4)", name, role, desc, tag)
# 3) Görev itemleri (upsert by code) + toplanabilir olanları location_resources'a
for code, name, rarity, loc, skill, step, xp, about in QITEMS:
iid = await c.fetchval("""
INSERT INTO items(code,name,type,rarity,base_value,description)
VALUES($1,$2,'material',$3,$4,$5)
ON CONFLICT(code) DO UPDATE SET name=EXCLUDED.name,rarity=EXCLUDED.rarity,
description=EXCLUDED.description RETURNING id
""", code, name, rarity, {"common":8,"uncommon":20,"rare":60,"epic":180}.get(rarity,8), about)
if loc:
lid = await c.fetchval("SELECT id FROM locations WHERE code=$1", loc)
if lid:
await c.execute("""
INSERT INTO location_resources(location_id,item_code,skill,step_cost,xp,cooldown_s)
VALUES($1,$2,$3,$4,$5,300)
ON CONFLICT(location_id,item_code) DO NOTHING
""", lid, code, skill, step, xp)
# 4) Quest'ler — önce eski seed'i temizle, sonra ekle
await c.execute("DELETE FROM quests WHERE author=$1", SEED_TAG)
qn = 0
for i, (title, gkey, region, summary, objs) in enumerate(QUESTS):
gid = giver_ids.get(gkey)
rtag = await resolve_map_tag(c, region)
qid = await c.fetchval("""
INSERT INTO quests(title,quest_type,summary,giver_npc_id,region_tag_id,author,sort_order)
VALUES($1,'side',$2,$3,$4,$5,$6) RETURNING id
""", title, summary, gid, rtag, SEED_TAG, i)
for j, note in enumerate(objs):
await c.execute("INSERT INTO quest_objectives(quest_id,note,ordering,map_tag_id) "
"VALUES($1,$2,$3,$4)", qid, note, j, rtag)
qn += 1
tot_q = await c.fetchval("SELECT count(*) FROM quests")
tot_npc = await c.fetchval("SELECT count(*) FROM npcs")
tot_hostile = await c.fetchval("SELECT count(*) FROM npcs WHERE stance='hostile'")
tot_items = await c.fetchval("SELECT count(*) FROM items")
await c.close()
print(f"Görev veren: {len(GIVERS)}, yeni düşman: {len(ENEMIES)}, görev itemi: {len(QITEMS)}")
print(f"Yan görev eklendi: {qn}. DB toplam quest: {tot_q}, NPC: {tot_npc} "
f"(hostile {tot_hostile}), item: {tot_items}")
asyncio.run(main())

249
db/seed_world_graph.py Normal file
View file

@ -0,0 +1,249 @@
"""
Örnek dünya: 15 lokasyon + 21 yol + lokasyon kaynakları + 12 D&D olayı.
Idempotent (code üzerinden upsert). Harita koordinatları 0-1000 (statik görsel için).
Yeni ham maddeler (13 odun, cevherler, otlar, balıklar, av) lokasyonlara tier'a göre dağıtılır.
Çalıştırma: cd /opt/stepstead && ./venv/bin/python3 db/seed_world_graph.py
"""
import asyncio
import json
import sys
import asyncpg
sys.path.insert(0, "/opt/stepstead")
from config import settings
DSN = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")
# (code, ad, tür, açıklama, x, y)
LOCATIONS = [
("koy", "Stepstead Köyü", "village", "Yolculuğun başladığı huzurlu köy. Etrafta taş, sopa ve sarmaşık bulunur.", 163, 186),
("pazar", "Pazarköy", "city", "Tüccarların uğrak yeri, canlı bir kasaba.", 232, 392),
("liman", "Liman Kenti", "city", "Büyük gemilerin yanaştığı ticaret limanı. Derin deniz balıılığı buradan yapılır.", 140, 630),
("fisilti_ormani", "Fısıltı Ormanı", "forest", "Köye en yakın orman. Meşe ve çam ağaçları.", 205, 110),
("gumus_camlik", "Gümüş Çamlık", "forest", "Sisli tepelerde huş ve kayın ağaçları.", 110, 300),
("akca_koru", "Akça Koru", "forest", "Akçaağaç ve söğütleriyle ünlü sakin koru.", 282, 255),
("kadim_orman", "Kadim Orman", "forest", "Yüzyıllık dişbudak, sedir ve ceviz ağaçları.", 320, 468),
("karanlik_orman", "Karaağaç Karanlığı", "forest", "En değerli ağaçların büyüdüğü tekinsiz orman: porsuk, maun, abanoz...", 262, 600),
("tas_ocagi", "Taş Ocağı", "mine", "Bakır ve kalay damarları, bol kömür.", 610, 290),
("derin_maden", "Derin Maden", "mine", "Demir ve gümüş çıkarılan eski maden.", 750, 320),
("ejder_dagi", "Ejder Dağı", "mine", "Altın damarlarının saklandığı efsanevi zirve.", 790, 115),
("aynali_gol", "Aynalı Göl", "lake", "Sazan ve alabalığın bol olduğu berrak göl.", 723, 655),
("firtina_kiyisi", "Fırtına Kıyısı", "lake", "Dalgalı denizde uskumru, palamut ve lüfer.", 685, 905),
("cayirlar", "Uçsuz Çayırlar", "meadow", "Şifalı otların yetiştiği rüzgârlı çayırlar.", 590, 760),
("av_topraklari", "Av Toprakları", "hunting", "Tavşan ve geyik avlanır. Daha derinde kurt vadisi.", 855, 712),
]
# (a, b, adım, olay_şansı)
ROADS = [
("koy", "fisilti_ormani", 320, 0.30),
("koy", "tas_ocagi", 350, 0.30),
("koy", "aynali_gol", 380, 0.25),
("koy", "cayirlar", 400, 0.30),
("koy", "pazar", 450, 0.20),
("fisilti_ormani", "gumus_camlik", 400, 0.35),
("gumus_camlik", "akca_koru", 450, 0.35),
("akca_koru", "kadim_orman", 500, 0.40),
("kadim_orman", "karanlik_orman", 600, 0.50),
("gumus_camlik", "av_topraklari", 500, 0.40),
("av_topraklari", "karanlik_orman", 650, 0.50),
("tas_ocagi", "derin_maden", 450, 0.35),
("derin_maden", "ejder_dagi", 600, 0.45),
("aynali_gol", "pazar", 300, 0.20),
("pazar", "liman", 500, 0.25),
("liman", "firtina_kiyisi", 250, 0.25),
("aynali_gol", "firtina_kiyisi", 550, 0.35),
("cayirlar", "av_topraklari", 450, 0.35),
("cayirlar", "gumus_camlik", 350, 0.30),
("derin_maden", "liman", 550, 0.30),
("cayirlar", "tas_ocagi", 300, 0.25),
]
# lokasyon → [(item_code, skill, adım, xp, cooldown_s)]
RESOURCES = {
"koy": [
("stick", "foraging", 4, 3, 120), ("stone_small", "foraging", 5, 3, 120),
("vine", "foraging", 6, 4, 120), ("herb_mint", "foraging", 8, 6, 180),
],
"fisilti_ormani": [
("log_oak", "woodcutting", 12, 8, 240), ("log_pine", "woodcutting", 14, 10, 240),
("stick", "foraging", 4, 3, 120), ("vine", "foraging", 6, 4, 120),
("herb_sage", "foraging", 9, 7, 180),
],
"gumus_camlik": [
("log_birch", "woodcutting", 16, 12, 240), ("log_beech", "woodcutting", 18, 14, 240),
("herb_thyme", "foraging", 10, 8, 180), ("stick", "foraging", 4, 3, 120),
],
"akca_koru": [
("log_maple", "woodcutting", 20, 16, 300), ("log_willow", "woodcutting", 22, 18, 300),
("herb_chamomile", "foraging", 11, 9, 180), ("vine", "foraging", 6, 4, 120),
],
"kadim_orman": [
("log_ash", "woodcutting", 24, 20, 300), ("log_cedar", "woodcutting", 26, 22, 300),
("log_walnut", "woodcutting", 28, 24, 300), ("herb_nettle", "foraging", 12, 10, 180),
],
"karanlik_orman": [
("log_cherry", "woodcutting", 30, 26, 360), ("log_yew", "woodcutting", 34, 30, 360),
("log_mahogany", "woodcutting", 38, 34, 360), ("log_ebony", "woodcutting", 42, 38, 360),
("herb_ginseng", "foraging", 16, 14, 240), ("herb_saffron", "foraging", 18, 16, 240),
],
"tas_ocagi": [
("ore_copper", "mining", 15, 10, 240), ("ore_tin", "mining", 15, 10, 240),
("coal", "mining", 12, 8, 240), ("stone_small", "foraging", 5, 3, 120),
],
"derin_maden": [
("ore_iron", "mining", 20, 16, 300), ("coal", "mining", 12, 8, 240),
("ore_silver", "mining", 26, 22, 300),
],
"ejder_dagi": [
("ore_gold", "mining", 34, 30, 360), ("ore_silver", "mining", 26, 22, 300),
("ore_iron", "mining", 20, 16, 300),
],
"aynali_gol": [
("fish_carp", "fishing", 14, 9, 240), ("fish_perch", "fishing", 15, 10, 240),
("fish_trout", "fishing", 18, 14, 240), ("vine", "foraging", 6, 4, 120),
],
"firtina_kiyisi": [
("fish_anchovy", "fishing", 14, 9, 240), ("fish_mackerel", "fishing", 16, 12, 240),
("fish_bonito", "fishing", 18, 14, 240), ("fish_bluefish", "fishing", 20, 16, 240),
("fish_bream", "fishing", 20, 16, 240),
],
"liman": [
("fish_pike", "fishing", 24, 20, 300), ("fish_catfish", "fishing", 26, 22, 300),
("fish_salmon", "fishing", 28, 24, 300), ("fish_turbot", "fishing", 30, 26, 300),
("fish_tuna", "fishing", 34, 30, 360), ("fish_sturgeon", "fishing", 38, 34, 360),
],
"cayirlar": [
("herb_rosemary", "foraging", 10, 8, 180), ("herb_basil", "foraging", 10, 8, 180),
("herb_lemonbalm", "foraging", 12, 10, 180), ("herb_fennel", "foraging", 12, 10, 180),
("herb_lavender", "foraging", 14, 12, 180), ("stick", "foraging", 4, 3, 120),
],
"av_topraklari": [
("meat_rabbit", "hunting", 16, 12, 300), ("leather_rabbit", "hunting", 16, 12, 300),
("meat_deer", "hunting", 20, 16, 300), ("leather_deer", "hunting", 20, 16, 300),
("meat_boar", "hunting", 24, 20, 360), ("leather_boar", "hunting", 24, 20, 360),
("meat_wolf", "hunting", 28, 24, 360), ("leather_wolf", "hunting", 28, 24, 360),
("meat_bear", "hunting", 34, 30, 420), ("leather_bear", "hunting", 34, 30, 420),
],
"pazar": [
("herb_mint", "foraging", 8, 6, 180), ("stick", "foraging", 4, 3, 120),
],
}
# D&D olay kartları. choices: skill check'li {text, skill, dc, success{}, fail{}} ya da düz {text, outcome{}}
EVENTS = [
("gezgin_tacir", "Gezgin Tacir", "Yolda rengarenk arabasıyla bir tacirle karşılaşıyorsun. Sana el sallıyor.", [
{"text": "Selam ver ve sohbet et", "outcome": {"text": "Tacir sohbetten memnun, sana bir nane çayı ikram ediyor.", "items": [{"code": "tea_mint", "qty": 1}]}},
{"text": "Başını öne eğip yoluna devam et", "outcome": {"text": "Tacir omuz silkip uzaklaşıyor."}},
]),
("haydut_pususu", "Haydut Pususu", "Çalıların arasından iki haydut fırlıyor! 'Adımlarını ya da canını!'", [
{"text": "Savaş!", "skill": "combat", "dc": 12,
"success": {"text": "Haydutları püskürttün! Kaçarken düşürdükleri keseyi aldın.", "items": [{"code": "stone_small", "qty": 3}], "xp": {"skill": "combat", "amount": 25}},
"fail": {"text": "Dayak yedin ve kaçtın. Yorgunluktan adımların eridi.", "steps": -150}},
{"text": "Kaçmayı dene", "skill": "hunting", "dc": 10,
"success": {"text": "Çevik adımlarla izini kaybettirdin.", "xp": {"skill": "hunting", "amount": 15}},
"fail": {"text": "Yakalandın, keseni silkelediler.", "steps": -100}},
{"text": "Adımlarından 100 ver", "outcome": {"text": "Haydutlar parayı alıp gözden kayboldu.", "steps": -100}},
]),
("yarali_gezgin", "Yaralı Gezgin", "Yol kenarında ayağı burkulmuş bir gezgin oturuyor, yardım istiyor.", [
{"text": "Yarasını sar (Simya)", "skill": "alchemy", "dc": 8,
"success": {"text": "Gezgin minnetle teşekkür edip çantasından ot uzatıyor.", "items": [{"code": "herb_mint", "qty": 2}], "xp": {"skill": "alchemy", "amount": 20}},
"fail": {"text": "Elinden geleni yaptın ama pek işe yaramadı. Zaman kaybettin.", "steps": -30}},
{"text": "Görmezden gel", "outcome": {"text": "Vicdanın sızlasa da yoluna devam ettin."}},
]),
("kayip_sandik", "Kayıp Sandık", "Patikanın kenarında yarı gömülü eski bir sandık görüyorsun.", [
{"text": "Kazıp aç", "outcome": {"text": "İçinden işe yarar malzemeler çıktı!", "items": [{"code": "stone_small", "qty": 2}, {"code": "vine", "qty": 2}]}},
{"text": "Tuzak olabilir, dokunma", "outcome": {"text": "Temkinli davranıp uzaklaştın."}},
]),
("dev_ayi", "Dev Ayı", "Önünde iki ayağı üzerinde doğrulmuş kocaman bir ayı! Homurtusu yeri titretiyor.", [
{"text": "Savaş!", "skill": "combat", "dc": 15,
"success": {"text": "Zorlu bir mücadele sonunda ayıyı alt ettin!", "items": [{"code": "meat_bear", "qty": 1}, {"code": "leather_bear", "qty": 1}], "xp": {"skill": "combat", "amount": 40}},
"fail": {"text": "Ayı seni fena hırpaladı. Canını zor kurtardın.", "steps": -200}},
{"text": "Yavaşça geri çekil", "skill": "hunting", "dc": 8,
"success": {"text": "Ses çıkarmadan uzaklaştın.", "xp": {"skill": "hunting", "amount": 10}},
"fail": {"text": "Bir dala bastın! Ayı kovaladı, koşmaktan tükendin.", "steps": -120}},
]),
("mantar_halkasi", "Mantar Halkası", "Bir açıklıkta mükemmel daire çizen mantarlar... Ortasında bir şey parlıyor.", [
{"text": "Dikkatlice topla (Toplama)", "skill": "foraging", "dc": 10,
"success": {"text": "Halkanın ortasından nadir otlar topladın!", "items": [{"code": "herb_chamomile", "qty": 2}], "xp": {"skill": "foraging", "amount": 20}},
"fail": {"text": "Mantarlar zehirliymiş, başın döndü.", "steps": -60}},
{"text": "Uğursuzluktur, dokunma", "outcome": {"text": "Halkaya saygıyla selam verip geçtin."}},
]),
("eski_harabe", "Eski Harabe", "Sarmaşıklarla kaplı yıkık bir taş yapı. İçinden serin bir esinti geliyor.", [
{"text": "İçini araştır (İnşaat)", "skill": "construction", "dc": 12,
"success": {"text": "Sağlam taşlar ve eski aletler buldun!", "items": [{"code": "stone_small", "qty": 4}], "xp": {"skill": "construction", "amount": 25}},
"fail": {"text": "Bir taş kaydı, az daha altında kalıyordun. Hızla çıktın.", "steps": -80}},
{"text": "Dışarıdan bak, devam et", "outcome": {"text": "Harabeyi haritana not edip ilerledin."}},
]),
("nehir_gecidi", "Nehir Geçidi", "Yol taşkın bir dereyle kesilmiş. Sığ bir geçit var ama akıntı hızlı.", [
{"text": "Geçerken balık da yakala (Balıılık)", "skill": "fishing", "dc": 8,
"success": {"text": "Hem karşıya geçtin hem eline bir sazan takıldı!", "items": [{"code": "fish_carp", "qty": 1}], "xp": {"skill": "fishing", "amount": 15}},
"fail": {"text": "Ayağın kaydı, sırılsıklam oldun.", "steps": -40}},
{"text": "Uzun ama güvenli yoldan dolaş", "outcome": {"text": "Ekstra yürüdün ama kuru kaldın.", "steps": -50}},
]),
("gizemli_yasli", "Gizemli Yaşlı", "Bastonlu bir ihtiyar taşın üstünde oturmuş, sana bakıp gülümsüyor: 'Otların dilinden anlar mısın evlat?'", [
{"text": "Yanına otur ve dinle", "outcome": {"text": "Sana otların sırlarını anlattı. Bilgeleştin.", "xp": {"skill": "alchemy", "amount": 20}}},
{"text": "Kibarca reddet", "outcome": {"text": "İhtiyar gülümseyip gözlerini kapadı."}},
]),
("kirik_araba", "Kırık Araba", "Bir çiftçinin arabasının tekeri kırılmış, yükü yola saçılmış.", [
{"text": "Tamir et (İnşaat)", "skill": "construction", "dc": 10,
"success": {"text": "Tekeri taktın! Çiftçi teşekkür olarak sarmaşık ipi verdi.", "items": [{"code": "vine", "qty": 3}], "xp": {"skill": "construction", "amount": 20}},
"fail": {"text": "Uğraştın ama beceremedin. Çiftçi yine de teşekkür etti.", "steps": -30}},
{"text": "Yardım edemem, acelem var", "outcome": {"text": "Çiftçi arkandan söylendi."}},
]),
("saganak", "Sağanak", "Gökyüzü kararıyor, bardaktan boşanırcasına yağmur başlıyor!", [
{"text": "Hızlıca sığınak yap (İnşaat)", "skill": "construction", "dc": 8,
"success": {"text": "Dallardan derme çatma ama işlevsel bir sığınak! Yağmuru kuru geçirdin.", "xp": {"skill": "construction", "amount": 15}},
"fail": {"text": "Sığınak çöktü, iliklerine kadar ıslandın.", "steps": -60}},
{"text": "Yağmurda yürümeye devam et", "outcome": {"text": "Islandın, üşüdün, yoruldun ama yol aldın.", "steps": -30}},
]),
("sansli_bulus", "Şanslı Buluş", "Patikanın hemen kenarında birinin unuttuğu küçük bir malzeme yığını duruyor.", [
{"text": "Topla", "outcome": {"text": "Sopa ve taşları sırt çantana koydun.", "items": [{"code": "stick", "qty": 2}, {"code": "stone_small", "qty": 2}]}},
{"text": "Sahibi gelir, bırak", "outcome": {"text": "Dürüstlüğünle gurur duyarak ilerledin."}},
]),
]
async def main():
conn = await asyncpg.connect(DSN)
for code, name, typ, desc, x, y in LOCATIONS:
await conn.execute("""
INSERT INTO locations (code, name, type, description, map_x, map_y)
VALUES ($1,$2,$3,$4,$5,$6)
ON CONFLICT (code) DO UPDATE SET name=$2, type=$3, description=$4, map_x=$5, map_y=$6
""", code, name, typ, desc, x, y)
ids = {r["code"]: r["id"] for r in await conn.fetch("SELECT id, code FROM locations")}
for a, b, cost, chance in ROADS:
await conn.execute("""
INSERT INTO roads (loc_a, loc_b, step_cost, event_chance)
VALUES ($1,$2,$3,$4)
ON CONFLICT (loc_a, loc_b) DO UPDATE SET step_cost=$3, event_chance=$4
""", ids[a], ids[b], cost, chance)
n_res = 0
for loc, res in RESOURCES.items():
for item, skill, steps, xp, cd in res:
await conn.execute("""
INSERT INTO location_resources (location_id, item_code, skill, step_cost, xp, cooldown_s)
VALUES ($1,$2,$3,$4,$5,$6)
ON CONFLICT (location_id, item_code) DO UPDATE SET skill=$3, step_cost=$4, xp=$5, cooldown_s=$6
""", ids[loc], item, skill, steps, xp, cd)
n_res += 1
for code, name, txt, choices in EVENTS:
await conn.execute("""
INSERT INTO events (code, name, text, choices) VALUES ($1,$2,$3,$4::jsonb)
ON CONFLICT (code) DO UPDATE SET name=$2, text=$3, choices=$4::jsonb
""", code, name, txt, json.dumps(choices))
# Konumu olmayan karakterleri köye koy
moved = await conn.execute(
"UPDATE characters SET location_id=$1 WHERE location_id IS NULL", ids["koy"])
print(f"Lokasyon: {len(LOCATIONS)}, yol: {len(ROADS)}, kaynak: {n_res}, olay: {len(EVENTS)}. Karakter taşındı: {moved}")
await conn.close()
asyncio.run(main())