McRogueFace/games/unwritten/systems/shop.py

230 lines
7.8 KiB
Python
Raw Normal View History

"""UNWRITTEN - Griselda's shop. Owner: Agent C.
HOOKS["shop"]() opens a two-column buy/sell UI over the current scene.
Stock per act, x1.25 prices under griselda_grudge, buyback at half, Chalk limited
to ONE ever (flag chalk_bought). Gold is called cogs. Esc leaves.
data.items is Agent B's (parallel); imported LAZILY. Expected interface:
data.items.ITEMS[item_id] = {
"name": str, "sprite": int, "cost": int,
"kind": "consumable"|"weapon"|"trinket"|"key",
"shop": bool, # sold by Griselda
"act": int, # earliest act it appears in stock (default 1)
}
"""
import math
import mcrfpy
from core import palette
from core import ui
from core.inputstack import InputStack
from systems import dialogue
from systems.state import GS
_MENU = None
def _items():
from data import items
tbl = getattr(items, "ITEMS", None)
if tbl is None:
raise RuntimeError("shop needs data.items.ITEMS (see module docstring)")
return tbl
def _price(item_id):
base = int(_items()[item_id]["cost"])
if GS.has("griselda_grudge"):
return int(math.ceil(base * 1.25))
return base
def _sell_price(item_id):
return int(_items()[item_id]["cost"]) // 2
class _Shop:
def __init__(self, scene, stack, on_close):
self.scene = scene
self.on_close = on_close
self.owns_stack = stack is None
self.stack = stack if stack is not None else InputStack(scene)
self.ch = scene.children
self.mode = "buy" # "buy" | "sell"
self.buy_menu = None
self.sell_menu = None
self.panel = None
self.gold_cap = None
self._build()
self.stack.push(self._handle, "shop")
# ----------------------------------------------------------------- build
def _build(self):
self.panel = ui.Panel(self.ch, (72, 60), (880, 640),
outline_color=palette.GOLD, z_index=4000)
p = self.panel.children
ui.Label(p, "GRISELDA'S", (32, 22), color=palette.GOLD,
size=palette.NAME_SIZE)
ui.Label(p, "forty years of sharpening. buy something.", (32, 48),
color=palette.DIM, size=palette.SMALL_SIZE)
self.gold_cap = ui.Label(p, "", (880 - 32, 22), color=palette.GOLD,
size=palette.NAME_SIZE)
self._refresh_gold()
ui.Label(p, "BUY", (170, 84), color=palette.PARCH, size=palette.BODY_SIZE,
center=True)
ui.Label(p, "SELL", (620, 84), color=palette.PARCH, size=palette.BODY_SIZE,
center=True)
ui.Label(p, "Tab: switch column Enter: confirm Esc: leave",
(440, 612), color=palette.DIM, size=palette.SMALL_SIZE, center=True)
self._rebuild_lists()
def _refresh_gold(self):
self.gold_cap.text = "%d cogs" % GS.gold
self.gold_cap.x = 880 - 32 - self.gold_cap.size.x
def _stock_ids(self):
# data.items exposes stock per act via SHOP_STOCK (act -> [ids]); the
# older per-item "shop"/"act" keys this shop was first coded against
# never existed. Use the real table (Agent D integration).
from data import items
stock = getattr(items, "SHOP_STOCK", None)
if stock is None:
raise RuntimeError("shop needs data.items.SHOP_STOCK (act -> [ids])")
tbl = _items()
act = GS.act if GS.act in stock else max(stock)
return [iid for iid in stock[act] if iid in tbl]
def _rebuild_lists(self):
p = self.panel.children
if self.buy_menu is not None:
self.buy_menu.destroy()
if self.sell_menu is not None:
self.sell_menu.destroy()
buy_items = []
for iid in self._stock_ids():
d = _items()[iid]
enabled, reason = True, ""
price = _price(iid)
if iid == "chalk" and GS.has("chalk_bought"):
enabled, reason = False, "sold out"
elif GS.gold < price:
enabled, reason = False, "need %d" % price
label = "%-14s %d" % (d["name"], price)
buy_items.append((label, iid, enabled, reason))
if not buy_items:
buy_items = [("(nothing in stock)", None, False, "")]
self.buy_menu = ui.MenuList(p, (40, 104), 360, buy_items,
on_pick=self._buy, on_cancel=self._leave)
sell_items = []
for iid, n in sorted(GS.inventory.items()):
if iid not in _items():
continue
d = _items()[iid]
if d.get("kind") == "key":
continue
label = "%-12s x%d %d" % (d["name"], n, _sell_price(iid))
sell_items.append((label, iid, True, ""))
if not sell_items:
sell_items = [("(nothing to sell)", None, False, "")]
self.sell_menu = ui.MenuList(p, (490, 104), 360, sell_items,
on_pick=self._sell, on_cancel=self._leave)
self._highlight()
def _highlight(self):
self.buy_menu.panel.frame.outline_color = (
palette.GOLD if self.mode == "buy" else palette.OUTLINE)
self.sell_menu.panel.frame.outline_color = (
palette.GOLD if self.mode == "sell" else palette.OUTLINE)
# ----------------------------------------------------------------- actions
def _buy(self, iid):
if iid is None:
return
price = _price(iid)
if not GS.spend_gold(price):
return
GS.grant(iid, 1)
if iid == "chalk":
GS.add_flag("chalk_bought")
ui.Toast(self.ch, "Bought %s" % _items()[iid]["name"], color=palette.GOLD)
self._refresh_gold()
self._rebuild_lists()
def _sell(self, iid):
if iid is None:
return
if GS.inventory.get(iid, 0) <= 0:
return
gain = _sell_price(iid)
GS.take(iid, 1)
GS.add_gold(gain)
ui.Toast(self.ch, "Sold %s (+%d)" % (_items()[iid]["name"], gain),
color=palette.GRASS)
self._refresh_gold()
self._rebuild_lists()
# ----------------------------------------------------------------- input
def _handle(self, key, state):
if state != mcrfpy.InputState.PRESSED:
return True
K = mcrfpy.Key
if key == K.ESCAPE:
self._leave()
return True
if key == K.TAB:
self.mode = "sell" if self.mode == "buy" else "buy"
self._highlight()
return True
active = self.buy_menu if self.mode == "buy" else self.sell_menu
active.handle(key, state)
return True
def _leave(self):
self.destroy()
if self.on_close:
self.on_close()
def destroy(self):
self.stack.pop("shop")
if self.buy_menu is not None:
self.buy_menu.destroy()
if self.sell_menu is not None:
self.sell_menu.destroy()
if self.panel is not None:
self.panel.destroy()
def open_shop(scene=None, stack=None, on_close=None):
global _MENU
scene = scene if scene is not None else mcrfpy.current_scene
_MENU = _Shop(scene, stack, on_close)
return _MENU
def _hook():
# opened from dialogue on the overworld: reuse the overworld's input stack
stack = None
try:
from systems import overworld
if overworld.WORLD is not None:
stack = overworld.WORLD.stack
def _reopen():
overworld.WORLD.frozen = False
overworld.WORLD.refresh_hud()
open_shop(mcrfpy.current_scene, stack, on_close=_reopen)
overworld.WORLD.frozen = True
return
except Exception:
pass
open_shop(mcrfpy.current_scene, stack, on_close=None)
def register_hooks():
dialogue.HOOKS["shop"] = _hook
register_hooks()