update shop costs

master
Denis Ranneft 1 month ago
parent b11b9bc437
commit bd1a636086

@ -1007,12 +1007,15 @@ func (h *GameHandler) InitHero(w http.ResponseWriter, r *http.Request) {
if hero == nil {
townsWithNPCs := h.buildTownsWithNPCs(r.Context())
pCost, hCost := tuning.EffectiveNPCShopCosts()
writeJSON(w, http.StatusOK, map[string]any{
"hero": nil,
"needsName": true,
"offlineReport": nil,
"mapRef": h.world.RefForLevel(1),
"towns": townsWithNPCs,
"npcCostPotion": pCost,
"npcCostHeal": hCost,
})
return
}
@ -1069,6 +1072,7 @@ func (h *GameHandler) InitHero(w http.ResponseWriter, r *http.Request) {
// Build towns with NPCs for the frontend map.
townsWithNPCs := h.buildTownsWithNPCs(r.Context())
pCost, hCost := tuning.EffectiveNPCShopCosts()
writeJSON(w, http.StatusOK, map[string]any{
"hero": hero,
@ -1076,6 +1080,8 @@ func (h *GameHandler) InitHero(w http.ResponseWriter, r *http.Request) {
"offlineReport": report,
"mapRef": h.world.RefForLevel(hero.Level),
"towns": townsWithNPCs,
"npcCostPotion": pCost,
"npcCostHeal": hCost,
})
}

@ -187,20 +187,20 @@ func (h *NPCHandler) InteractNPC(w http.ResponseWriter, r *http.Request) {
}
case "merchant":
cfg := tuning.Get()
potionCost, _ := tuning.EffectiveNPCShopCosts()
actions = append(actions, model.NPCInteractAction{
ActionType: "shop_item",
ItemName: "Healing Potion",
ItemCost: cfg.NPCCostPotion,
ItemCost: potionCost,
Description: "Restores health. Always handy in a pinch.",
})
case "healer":
cfg := tuning.Get()
_, healCost := tuning.EffectiveNPCShopCosts()
actions = append(actions, model.NPCInteractAction{
ActionType: "heal",
ItemName: "Full Heal",
ItemCost: cfg.NPCCostHeal,
ItemCost: healCost,
Description: "Restore hero to full HP.",
})
}
@ -593,7 +593,7 @@ func (h *NPCHandler) HealHero(w http.ResponseWriter, r *http.Request) {
}
}
healCost := tuning.Get().NPCCostHeal
_, healCost := tuning.EffectiveNPCShopCosts()
if hero.Gold < healCost {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": fmt.Sprintf("not enough gold (need %d, have %d)", healCost, hero.Gold),
@ -643,7 +643,7 @@ func (h *NPCHandler) BuyPotion(w http.ResponseWriter, r *http.Request) {
return
}
potionCost := tuning.Get().NPCCostPotion
potionCost, _ := tuning.EffectiveNPCShopCosts()
if hero.Gold < potionCost {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": fmt.Sprintf("not enough gold (need %d, have %d)", potionCost, hero.Gold),

@ -361,6 +361,21 @@ func Get() Values {
return *p
}
// EffectiveNPCShopCosts returns potion and full-heal prices from runtime tuning (DB-merged JSON),
// falling back to defaults when unset or non-positive.
func EffectiveNPCShopCosts() (potionCost, healCost int64) {
cfg := Get()
potionCost = cfg.NPCCostPotion
if potionCost <= 0 {
potionCost = DefaultValues().NPCCostPotion
}
healCost = cfg.NPCCostHeal
if healCost <= 0 {
healCost = DefaultValues().NPCCostHeal
}
return potionCost, healCost
}
func Set(v Values) {
current.Store(&v)
}

@ -29,6 +29,8 @@ import {
buyPotion,
healAtNPC,
requestRevive,
defaultNpcShopCosts,
npcShopCostsFromInit,
} from './network/api';
import type { HeroResponse, Achievement } from './network/api';
import type { AdventureLogEntry, Town, HeroQuest, NPC, TownData, EquipmentItem, BuildingData } from './game/types';
@ -340,6 +342,7 @@ export function App() {
// Wandering NPC encounter state
const [wanderingNPC, setWanderingNPC] = useState<NPCEncounterEvent | null>(null);
const [npcShopCosts, setNpcShopCosts] = useState(defaultNpcShopCosts);
// Achievements
const [achievements, setAchievements] = useState<Achievement[]>([]);
const prevAchievementsRef = useRef<Achievement[]>([]);
@ -424,6 +427,7 @@ export function App() {
try {
const telegramId = getTelegramUserId() ?? 1;
const initRes = await initHero(telegramId);
setNpcShopCosts(npcShopCostsFromInit(initRes));
// Gate game start behind name entry — no hero row until POST /hero/name
if (initRes.needsName) {
@ -1067,7 +1071,7 @@ export function App() {
buyPotion(telegramId)
.then((hero) => {
hapticImpact('medium');
setToast({ message: t(tr.boughtPotion, { cost: 50 }), color: '#88dd88' });
setToast({ message: t(tr.boughtPotion, { cost: npcShopCosts.potionCost }), color: '#88dd88' });
handleNPCHeroUpdated(hero);
// Server logs purchase + WS
})
@ -1082,7 +1086,7 @@ export function App() {
}
}
});
}, [handleNPCHeroUpdated]);
}, [handleNPCHeroUpdated, npcShopCosts.potionCost, tr]);
const handleNPCHeal = useCallback((npc: NPCData) => {
const telegramId = getTelegramUserId() ?? 1;
@ -1237,6 +1241,8 @@ export function App() {
<NPCInteraction
npc={nearestNPC}
heroGold={gameState.hero?.gold ?? 0}
potionCost={npcShopCosts.potionCost}
healCost={npcShopCosts.healCost}
onViewQuests={handleNPCViewQuests}
onBuyPotion={handleNPCBuyPotion}
onHeal={handleNPCHeal}
@ -1250,6 +1256,8 @@ export function App() {
npc={selectedNPC}
heroQuests={heroQuests}
heroGold={gameState.hero?.gold ?? 0}
potionCost={npcShopCosts.potionCost}
healCost={npcShopCosts.healCost}
onClose={() => setSelectedNPC(null)}
onQuestsChanged={refreshHeroQuests}
onHeroUpdated={handleNPCHeroUpdated}

@ -186,6 +186,25 @@ export interface InitHeroResponse {
offlineReport: OfflineReport | null;
mapRef: MapRefResponse;
needsName?: boolean;
/** Runtime tuning: merchant potion price (from DB / runtime_config). */
npcCostPotion?: number;
/** Runtime tuning: healer full heal price (from DB / runtime_config). */
npcCostHeal?: number;
}
/** Matches server defaults when init omits costs (must stay in sync with tuning.DefaultValues). */
export function defaultNpcShopCosts(): { potionCost: number; healCost: number } {
return { potionCost: 50, healCost: 100 };
}
export function npcShopCostsFromInit(res: InitHeroResponse): { potionCost: number; healCost: number } {
const d = defaultNpcShopCosts();
const p = res.npcCostPotion;
const h = res.npcCostHeal;
return {
potionCost: typeof p === 'number' && p > 0 ? p : d.potionCost,
healCost: typeof h === 'number' && h > 0 ? h : d.healCost,
};
}
/** Initialize or retrieve a hero from the backend (creates on first call) */

@ -12,6 +12,8 @@ interface NPCDialogProps {
npc: NPC;
heroQuests: HeroQuest[];
heroGold: number;
potionCost: number;
healCost: number;
onClose: () => void;
onQuestsChanged: () => void;
onHeroUpdated: (hero: HeroResponse) => void;
@ -228,17 +230,14 @@ function questTypeIcon(type: string): string {
}
}
// ---- Constants ----
const POTION_COST = 50;
const HEAL_COST = 30;
// ---- Component ----
export function NPCDialog({
npc,
heroQuests,
heroGold,
potionCost,
healCost,
onClose,
onQuestsChanged,
onHeroUpdated,
@ -308,24 +307,24 @@ export function NPCDialog({
);
const handleBuyPotion = useCallback(() => {
if (heroGold < POTION_COST) {
if (heroGold < potionCost) {
onToast(tr.notEnoughGold, '#ff4444');
return;
}
buyPotion(telegramId)
.then((hero) => {
hapticImpact('medium');
onToast(t(tr.boughtPotion, { cost: POTION_COST }), '#88dd88');
onToast(t(tr.boughtPotion, { cost: potionCost }), '#88dd88');
onHeroUpdated(hero);
})
.catch((err) => {
console.warn('[NPCDialog] Failed to buy potion:', err);
onToast(tr.failedToBuyPotion, '#ff4444');
});
}, [telegramId, heroGold, onHeroUpdated, onToast]);
}, [telegramId, heroGold, potionCost, onHeroUpdated, onToast, tr]);
const handleHeal = useCallback(() => {
if (heroGold < HEAL_COST) {
if (heroGold < healCost) {
onToast(tr.notEnoughGold, '#ff4444');
return;
}
@ -339,7 +338,7 @@ export function NPCDialog({
console.warn('[NPCDialog] Failed to heal:', err);
onToast(tr.failedToHeal, '#ff4444');
});
}, [telegramId, heroGold, onHeroUpdated, onToast, npc.id]);
}, [telegramId, heroGold, healCost, onHeroUpdated, onToast, npc.id, tr]);
// Quests relevant to this NPC
const npcHeroQuests = heroQuests.filter(
@ -522,14 +521,14 @@ export function NPCDialog({
<div style={sectionTitleStyle}>Shop</div>
<button
style={
heroGold >= POTION_COST
heroGold >= potionCost
? { ...serviceBtnStyle, backgroundColor: 'rgba(68, 200, 68, 0.2)', color: '#88dd88' }
: { ...disabledBtnStyle, backgroundColor: 'rgba(68, 200, 68, 0.1)', color: '#88dd88' }
}
onClick={handleBuyPotion}
disabled={heroGold < POTION_COST}
disabled={heroGold < potionCost}
>
{'\uD83E\uDDEA'} {tr.buyPotion} &mdash; {POTION_COST} {tr.gold}
{'\uD83E\uDDEA'} {tr.buyPotion} &mdash; {potionCost} {tr.gold}
</button>
<div style={{ fontSize: 11, color: '#666', textAlign: 'center' }}>
Your gold: {heroGold}
@ -543,14 +542,14 @@ export function NPCDialog({
<div style={sectionTitleStyle}>Services</div>
<button
style={
heroGold >= HEAL_COST
heroGold >= healCost
? { ...serviceBtnStyle, backgroundColor: 'rgba(200, 68, 68, 0.2)', color: '#ff8888' }
: { ...disabledBtnStyle, backgroundColor: 'rgba(200, 68, 68, 0.1)', color: '#ff8888' }
}
onClick={handleHeal}
disabled={heroGold < HEAL_COST}
disabled={heroGold < healCost}
>
{'\u2764\uFE0F'} {tr.healToFull} &mdash; {HEAL_COST} {tr.gold}
{'\u2764\uFE0F'} {tr.healToFull} &mdash; {healCost} {tr.gold}
</button>
<div style={{ fontSize: 11, color: '#666', textAlign: 'center' }}>
Your gold: {heroGold}

@ -6,6 +6,8 @@ import type { NPCData } from '../game/types';
interface NPCInteractionProps {
npc: NPCData;
heroGold: number;
potionCost: number;
healCost: number;
onViewQuests: (npc: NPCData) => void;
onBuyPotion: (npc: NPCData) => void;
onHeal: (npc: NPCData) => void;
@ -77,9 +79,6 @@ const actionBtnStyle: CSSProperties = {
textAlign: 'center',
};
const POTION_COST = 50;
const HEAL_COST = 30;
// ---- NPC appearance ----
function npcColor(type: string): { bg: string; icon: string; text: string } {
@ -100,6 +99,8 @@ function npcColor(type: string): { bg: string; icon: string; text: string } {
export function NPCInteraction({
npc,
heroGold,
potionCost,
healCost,
onViewQuests,
onBuyPotion,
onHeal,
@ -126,9 +127,9 @@ export function NPCInteraction({
case 'quest_giver':
return 'View Quests';
case 'merchant':
return `Buy Potion (${POTION_COST}g)`;
return `Buy Potion (${potionCost}g)`;
case 'healer':
return `Heal to Full (${HEAL_COST}g)`;
return `Heal to Full (${healCost}g)`;
default:
return 'Talk';
}
@ -136,8 +137,8 @@ export function NPCInteraction({
const canAfford =
npc.type === 'quest_giver' ||
(npc.type === 'merchant' && heroGold >= POTION_COST) ||
(npc.type === 'healer' && heroGold >= HEAL_COST);
(npc.type === 'merchant' && heroGold >= potionCost) ||
(npc.type === 'healer' && heroGold >= healCost);
return (
<>

Loading…
Cancel
Save