You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

294 lines
8.1 KiB
Go

package handler
import (
"log/slog"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/denisovdennis/autohero/internal/storage"
)
// QuestHandler serves quest system API endpoints.
type QuestHandler struct {
questStore *storage.QuestStore
heroStore *storage.HeroStore
logStore *storage.LogStore
logger *slog.Logger
}
// NewQuestHandler creates a new QuestHandler.
func NewQuestHandler(questStore *storage.QuestStore, heroStore *storage.HeroStore, logStore *storage.LogStore, logger *slog.Logger) *QuestHandler {
return &QuestHandler{
questStore: questStore,
heroStore: heroStore,
logStore: logStore,
logger: logger,
}
}
// ListTowns returns all towns.
// GET /api/v1/towns
func (h *QuestHandler) ListTowns(w http.ResponseWriter, r *http.Request) {
towns, err := h.questStore.ListTowns(r.Context())
if err != nil {
h.logger.Error("failed to list towns", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to list towns",
})
return
}
writeJSON(w, http.StatusOK, towns)
}
// ListNPCsByTown returns all NPCs in a town.
// GET /api/v1/towns/{townId}/npcs
func (h *QuestHandler) ListNPCsByTown(w http.ResponseWriter, r *http.Request) {
townIDStr := chi.URLParam(r, "townId")
townID, err := strconv.ParseInt(townIDStr, 10, 64)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid townId",
})
return
}
npcs, err := h.questStore.ListNPCsByTown(r.Context(), townID)
if err != nil {
h.logger.Error("failed to list npcs", "town_id", townID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to list npcs",
})
return
}
writeJSON(w, http.StatusOK, npcs)
}
// ListQuestsByNPC returns all quests offered by an NPC.
// GET /api/v1/npcs/{npcId}/quests
func (h *QuestHandler) ListQuestsByNPC(w http.ResponseWriter, r *http.Request) {
npcIDStr := chi.URLParam(r, "npcId")
npcID, err := strconv.ParseInt(npcIDStr, 10, 64)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid npcId",
})
return
}
quests, err := h.questStore.ListQuestsByNPC(r.Context(), npcID)
if err != nil {
h.logger.Error("failed to list quests", "npc_id", npcID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to list quests",
})
return
}
writeJSON(w, http.StatusOK, quests)
}
// AcceptQuest accepts a quest for the hero.
// POST /api/v1/hero/quests/{questId}/accept
func (h *QuestHandler) AcceptQuest(w http.ResponseWriter, r *http.Request) {
telegramID, ok := resolveTelegramID(r)
if !ok {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "missing telegramId",
})
return
}
questIDStr := chi.URLParam(r, "questId")
questID, err := strconv.ParseInt(questIDStr, 10, 64)
if err != nil || questID == 0 {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid questId",
})
return
}
req := struct{ QuestID int64 }{QuestID: questID}
hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID)
if err != nil {
h.logger.Error("failed to get hero for quest accept", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to load hero",
})
return
}
if hero == nil {
writeJSON(w, http.StatusNotFound, map[string]string{
"error": "hero not found",
})
return
}
if err := h.questStore.AcceptQuest(r.Context(), hero.ID, req.QuestID); err != nil {
h.logger.Error("failed to accept quest", "hero_id", hero.ID, "quest_id", req.QuestID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to accept quest",
})
return
}
h.logger.Info("quest accepted", "hero_id", hero.ID, "quest_id", req.QuestID)
writeJSON(w, http.StatusOK, map[string]string{
"status": "accepted",
})
}
// ListHeroQuests returns the hero's quest log.
// GET /api/v1/hero/quests
func (h *QuestHandler) ListHeroQuests(w http.ResponseWriter, r *http.Request) {
telegramID, ok := resolveTelegramID(r)
if !ok {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "missing telegramId",
})
return
}
hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID)
if err != nil {
h.logger.Error("failed to get hero for quest list", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to load hero",
})
return
}
if hero == nil {
writeJSON(w, http.StatusNotFound, map[string]string{
"error": "hero not found",
})
return
}
quests, err := h.questStore.ListHeroQuests(r.Context(), hero.ID)
if err != nil {
h.logger.Error("failed to list hero quests", "hero_id", hero.ID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to list quests",
})
return
}
writeJSON(w, http.StatusOK, quests)
}
// ClaimQuestReward claims a completed quest's reward.
// POST /api/v1/hero/quests/{questId}/claim
func (h *QuestHandler) ClaimQuestReward(w http.ResponseWriter, r *http.Request) {
telegramID, ok := resolveTelegramID(r)
if !ok {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "missing telegramId",
})
return
}
questIDStr := chi.URLParam(r, "questId")
questID, err := strconv.ParseInt(questIDStr, 10, 64)
if err != nil {
h.logger.Error("Error claiming quest", err)
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid questId",
})
return
}
hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID)
if err != nil {
h.logger.Error("failed to get hero for quest claim", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to load hero",
})
return
}
if hero == nil {
writeJSON(w, http.StatusNotFound, map[string]string{
"error": "hero not found",
})
return
}
reward, err := h.questStore.ClaimQuestReward(r.Context(), hero.ID, questID)
if err != nil {
h.logger.Warn("failed to claim quest reward", "hero_id", hero.ID, "quest_id", questID, "error", err)
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": err.Error(),
})
return
}
// Apply rewards to hero.
hero.XP += reward.XP
hero.Gold += reward.Gold
hero.Potions += reward.Potions
// Run level-up loop.
for hero.LevelUp() {
}
if err := h.heroStore.Save(r.Context(), hero); err != nil {
h.logger.Error("failed to save hero after quest claim", "hero_id", hero.ID, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to save hero",
})
return
}
h.logger.Info("quest reward claimed", "hero_id", hero.ID, "quest_id", questID,
"xp", reward.XP, "gold", reward.Gold, "potions", reward.Potions)
writeJSON(w, http.StatusOK, hero)
}
// AbandonQuest removes a quest from the hero's quest log.
// DELETE /api/v1/hero/quests/{questId}
func (h *QuestHandler) AbandonQuest(w http.ResponseWriter, r *http.Request) {
telegramID, ok := resolveTelegramID(r)
if !ok {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "missing telegramId",
})
return
}
questIDStr := chi.URLParam(r, "questId")
questID, err := strconv.ParseInt(questIDStr, 10, 64)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "invalid questId",
})
return
}
hero, err := h.heroStore.GetByTelegramID(r.Context(), telegramID)
if err != nil {
h.logger.Error("failed to get hero for quest abandon", "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "failed to load hero",
})
return
}
if hero == nil {
writeJSON(w, http.StatusNotFound, map[string]string{
"error": "hero not found",
})
return
}
if err := h.questStore.AbandonQuest(r.Context(), hero.ID, questID); err != nil {
h.logger.Warn("failed to abandon quest", "hero_id", hero.ID, "quest_id", questID, "error", err)
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": err.Error(),
})
return
}
h.logger.Info("quest abandoned", "hero_id", hero.ID, "quest_id", questID)
writeJSON(w, http.StatusOK, map[string]string{
"status": "abandoned",
})
}