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
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",
|
|
})
|
|
}
|