mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Add masterwork rework, arena combat log, Death's Reprieve fix, and wordle improvements
Adventure — Masterwork Rework: - Expand from 3 generic items to 15 tiered masterwork items across mining (weapon), fishing (armor), and foraging (boots) with per-tier drop rates (5%/4%/3%/2%/1.5%) and location gating - Skill-specific cross-skill bonuses replace flat masterwork check - Auto-equip if better, silent discard for duplicates, inventory for rest - Add !adventure equip command for manual masterwork equipping - Tiered room announcements: T1-2 DM only, T3 quiet, T4-5 full - First-drop detection with special message - Character sheet shows ⭐/⚔️ markers for masterwork/arena gear - Sell protection for special gear in shop Adventure — Arena Combat Log: - Turn-based Dragon Quest style narrative for arena fights - Outcome-first design: roll determines win/loss, log assembled backward - No-repeat flavor text via actionPicker with per-pool tracking - 60 participation XP on arena loss Adventure — Death's Reprieve Fix: - All equipment set to 1 condition instead of death-level degradation Wordle: - Refactored word sourcing and expanded fallback word list - Simplified Wordnik integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,175 +1,76 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"gogobee/internal/dreamclient"
|
||||
)
|
||||
|
||||
// wordnikRandomWordResponse is the Wordnik randomWord response.
|
||||
type wordnikRandomWordResponse struct {
|
||||
Word string `json:"word"`
|
||||
}
|
||||
|
||||
// wordnikDefinitionResponse is one definition entry.
|
||||
type wordnikDefinitionResponse struct {
|
||||
Text string `json:"text"`
|
||||
PartOfSpeech string `json:"partOfSpeech"`
|
||||
}
|
||||
|
||||
// wordnikFetchRandomWord fetches a random word of the given length from Wordnik.
|
||||
// Returns the uppercased word. Retries up to 5 times to avoid bad words.
|
||||
func wordnikFetchRandomWord(apiKey string, client *http.Client, wordLength int) (string, error) {
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
url := fmt.Sprintf(
|
||||
"https://api.wordnik.com/v4/words.json/randomWord?hasDictionaryDef=true&minLength=%d&maxLength=%d&minCorpusCount=5000&minDictionaryCount=3&api_key=%s",
|
||||
wordLength, wordLength, apiKey,
|
||||
)
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("wordle: create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("wordle: fetch random word: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("wordle: API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result wordnikRandomWordResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", fmt.Errorf("wordle: decode response: %w", err)
|
||||
}
|
||||
|
||||
word := strings.ToUpper(strings.TrimSpace(result.Word))
|
||||
|
||||
// Reject words with hyphens, spaces, apostrophes.
|
||||
if strings.ContainsAny(word, "-' ") {
|
||||
slog.Debug("wordle: rejecting word with special chars", "word", word, "attempt", attempt+1)
|
||||
continue
|
||||
}
|
||||
|
||||
// Reject if not all alphabetic.
|
||||
allAlpha := true
|
||||
for _, r := range word {
|
||||
if !unicode.IsLetter(r) {
|
||||
allAlpha = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if !allAlpha {
|
||||
slog.Debug("wordle: rejecting non-alpha word", "word", word, "attempt", attempt+1)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check definition to reject proper nouns.
|
||||
if isProperNoun(apiKey, client, word) {
|
||||
slog.Debug("wordle: rejecting proper noun", "word", word, "attempt", attempt+1)
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Info("wordle: selected word", "word", word, "length", wordLength, "attempt", attempt+1)
|
||||
return word, nil
|
||||
// dictValidateWord checks if a word exists in DreamDict.
|
||||
// Returns (valid, apiErr). apiErr is true when the service is unreachable.
|
||||
func dictValidateWord(dict *dreamclient.Client, word, lang string) (valid bool, apiErr bool) {
|
||||
if dict == nil {
|
||||
return true, false // no client configured = skip validation
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("wordle: failed to find suitable word after 5 attempts")
|
||||
}
|
||||
|
||||
// isProperNoun checks if a word's first definition starts with a capital letter.
|
||||
func isProperNoun(apiKey string, client *http.Client, word string) bool {
|
||||
defs, err := wordnikFetchDefinitions(apiKey, client, strings.ToLower(word), 1)
|
||||
if err != nil || len(defs) == 0 {
|
||||
return false
|
||||
}
|
||||
text := strings.TrimSpace(defs[0].Text)
|
||||
if text == "" {
|
||||
return false
|
||||
}
|
||||
return unicode.IsUpper([]rune(text)[0])
|
||||
}
|
||||
|
||||
// wordnikValidateWord checks if a word exists by looking up its definitions.
|
||||
// Returns valid bool and an error flag. On API errors, returns false with apiErr=true
|
||||
// so the caller can show a different message than "not a valid word".
|
||||
func wordnikValidateWord(apiKey string, client *http.Client, word string) (valid bool, apiErr bool) {
|
||||
if apiKey == "" {
|
||||
return true, false // no API key = skip validation
|
||||
}
|
||||
defs, err := wordnikFetchDefinitions(apiKey, client, strings.ToLower(word), 1)
|
||||
v, err := dict.IsValidWord(strings.ToLower(word), lang)
|
||||
if err != nil {
|
||||
slog.Warn("wordle: validation API error", "word", word, "err", err)
|
||||
slog.Warn("wordle: dictionary validation error", "word", word, "lang", lang, "err", err)
|
||||
return false, true
|
||||
}
|
||||
return len(defs) > 0, false
|
||||
return v, false
|
||||
}
|
||||
|
||||
// wordnikFetchDefinitionText fetches a clean definition string for display.
|
||||
func wordnikFetchDefinitionText(apiKey string, client *http.Client, word string) string {
|
||||
defs, err := wordnikFetchDefinitions(apiKey, client, strings.ToLower(word), 3)
|
||||
if err != nil || len(defs) == 0 {
|
||||
// dictFetchDefinitionText fetches a clean definition string for display.
|
||||
// Tries the given language first, falls back to English.
|
||||
func dictFetchDefinitionText(dict *dreamclient.Client, word, lang string) string {
|
||||
if dict == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Find the first clean definition.
|
||||
for _, d := range defs {
|
||||
text := strings.TrimSpace(d.Text)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
// Strip HTML tags if any.
|
||||
text = stripHTMLTags(text)
|
||||
pos := d.PartOfSpeech
|
||||
if pos != "" {
|
||||
return fmt.Sprintf("%s (%s): %s", strings.ToLower(word), pos, text)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", strings.ToLower(word), text)
|
||||
defs, err := dict.Define(strings.ToLower(word), lang)
|
||||
if err != nil {
|
||||
slog.Warn("wordle: definition fetch error", "word", word, "lang", lang, "err", err)
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
|
||||
// If no definitions in the puzzle language, try English.
|
||||
if len(defs) == 0 && lang != "en" {
|
||||
defs, err = dict.Define(strings.ToLower(word), "en")
|
||||
if err != nil || len(defs) == 0 {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
if len(defs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
d := defs[0]
|
||||
text := strings.TrimSpace(d.Gloss)
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
// Strip HTML tags if any.
|
||||
text = stripHTMLTags(text)
|
||||
pos := d.POS
|
||||
if pos != "" {
|
||||
return fmt.Sprintf("%s (%s): %s", strings.ToLower(word), pos, text)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", strings.ToLower(word), text)
|
||||
}
|
||||
|
||||
// wordnikFetchDefinitions fetches definitions from Wordnik.
|
||||
func wordnikFetchDefinitions(apiKey string, client *http.Client, word string, limit int) ([]wordnikDefinitionResponse, error) {
|
||||
url := fmt.Sprintf(
|
||||
"https://api.wordnik.com/v4/word.json/%s/definitions?limit=%d&sourceDictionaries=all&api_key=%s",
|
||||
word, limit, apiKey,
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// categoryLang returns the DreamDict language code for a Wordle category.
|
||||
func categoryLang(category WordleCategory) string {
|
||||
switch category {
|
||||
case WordleCategoryPT:
|
||||
return "pt-PT"
|
||||
case WordleCategoryFR:
|
||||
return "fr"
|
||||
default:
|
||||
return "en"
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, nil // word not found
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var defs []wordnikDefinitionResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&defs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return defs, nil
|
||||
}
|
||||
|
||||
// stripHTMLTags removes HTML tags from a string.
|
||||
|
||||
Reference in New Issue
Block a user