clean up helpers package, remove orderedmaps and clean up list generation

This commit is contained in:
Sky Johnson 2025-08-27 10:54:55 -05:00
parent fc30d04ccb
commit 41eed92929
15 changed files with 99 additions and 119 deletions

11
data/control.json Normal file
View File

@ -0,0 +1,11 @@
{
"world_size": 200,
"open": 1,
"admin_email": "",
"email_mode": "file",
"email_file_path": "data/emails.txt",
"smtp_host": "",
"smtp_port": "587",
"smtp_username": "",
"smtp_password": ""
}

Binary file not shown.

2
go.mod
View File

@ -5,7 +5,6 @@ go 1.25.0
require (
git.sharkk.net/Sharkk/Sashimi v1.1.4
git.sharkk.net/Sharkk/Sushi v1.2.0
github.com/valyala/fasthttp v1.65.0
)
require (
@ -17,6 +16,7 @@ require (
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.65.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/sys v0.35.0 // indirect

View File

@ -25,15 +25,11 @@ func LeftAside(ctx sushi.Ctx) map[string]any {
}
// Build owned town maps list
if user.Towns != "" {
townMap := helpers.NewOrderedMap[int, *towns.Town]()
for _, id := range user.GetTownIDs() {
if town, err := towns.Find(id); err == nil {
townMap.Set(id, town)
}
}
data["_towns"] = townMap.ToSlice()
townList, err := user.GetKnownTowns()
if err != nil {
fmt.Printf("User town list error: %s\n", err.Error())
}
data["_towns"] = townList
return data
}
@ -65,16 +61,11 @@ func RightAside(ctx sushi.Ctx) map[string]any {
}
// Build known healing spells list
userSpells, err := user.GetSpells()
if err == nil {
spellMap := helpers.NewOrderedMap[int, string]()
for _, spell := range userSpells {
if spell.IsHeal() {
spellMap.Set(spell.ID, spell.Name)
}
}
data["_spells"] = spellMap.ToSlice()
healingSpells, err := user.GetHealingSpells()
if err != nil {
fmt.Printf("User healing spells list error: %s\n", err.Error())
}
data["_spells"] = healingSpells
return data
}

View File

@ -1,5 +0,0 @@
package exp
func Calc(level int) int {
return level * level * level
}

View File

@ -1,10 +0,0 @@
package helpers
import "github.com/valyala/fasthttp"
// IsHTTPS tries to determine if the current request context is over HTTPS
func IsHTTPS(ctx *fasthttp.RequestCtx) bool {
return ctx.IsTLS() ||
string(ctx.Request.Header.Peek("X-Forwarded-Proto")) == "https" ||
string(ctx.Request.Header.Peek("X-Forwarded-Scheme")) == "https"
}

View File

@ -8,3 +8,8 @@ func ClampPct(num, denom, minimum, maximum float64) float64 {
}
return max(minimum, min(maximum, (num/denom)*100))
}
// ExpAtLevel calculates the needed total EXP for the given level.
func ExpAtLevel(level int) int {
return level * level * level
}

View File

@ -1,36 +0,0 @@
package helpers
type OrderedMap[K comparable, V any] struct {
keys []K
data map[K]V
}
func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] {
return &OrderedMap[K, V]{
keys: make([]K, 0),
data: make(map[K]V),
}
}
func (om *OrderedMap[K, V]) Set(key K, value V) {
if _, exists := om.data[key]; !exists {
om.keys = append(om.keys, key)
}
om.data[key] = value
}
func (om *OrderedMap[K, V]) Range(fn func(K, V) bool) {
for _, key := range om.keys {
if !fn(key, om.data[key]) {
break
}
}
}
func (om *OrderedMap[K, V]) ToSlice() []V {
result := make([]V, 0, len(om.keys))
for _, key := range om.keys {
result = append(result, om.data[key])
}
return result
}

View File

@ -116,8 +116,8 @@ func ByName(name string) (*Spell, error) {
func UnlocksForClassAtLevel(classID, level int) ([]*Spell, error) {
var spells []*Spell
query := `
SELECT s.* FROM spells s
JOIN spell_unlocks u ON s.id = u.spell_id
SELECT s.* FROM spells s
JOIN spell_unlocks u ON s.id = u.spell_id
WHERE u.class_id = %d AND u.level = %d
ORDER BY s.type ASC, s.mp ASC, s.id ASC`
err := database.Select(&spells, query, classID, level)
@ -135,6 +135,17 @@ func UserSpells(userID int) ([]*Spell, error) {
return spells, err
}
func UserHealingSpells(userID int) ([]*Spell, error) {
var spells []*Spell
query := `
SELECT s.* FROM spells s
JOIN user_spells us ON s.id = us.spell_id
WHERE us.user_id = %d AND s.type = 0
ORDER BY s.mp ASC, s.id ASC`
err := database.Select(&spells, query, userID)
return spells, err
}
func GrantSpell(userID, spellID int) error {
return database.Exec("INSERT OR IGNORE INTO user_spells (user_id, spell_id) VALUES (%d, %d)", userID, spellID)
}

View File

@ -8,6 +8,7 @@ import (
"dk/internal/database"
"dk/internal/helpers"
"dk/internal/models/items"
)
// Town represents a town in the game
@ -142,16 +143,16 @@ func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
}
// Helper methods
func (t *Town) GetShopItems() []int {
func (t *Town) GetShopItemIDs() []int {
return helpers.StringToInts(t.ShopList)
}
func (t *Town) SetShopItems(items []int) {
func (t *Town) SetShopItemIDs(items []int) {
t.ShopList = helpers.IntsToString(items)
}
func (t *Town) HasShopItem(itemID int) bool {
return slices.Contains(t.GetShopItems(), itemID)
return slices.Contains(t.GetShopItemIDs(), itemID)
}
func (t *Town) DistanceFromSquared(x, y int) float64 {
@ -181,7 +182,7 @@ func (t *Town) CanAffordTeleport(gold int) bool {
}
func (t *Town) HasShop() bool {
return len(t.GetShopItems()) > 0
return len(t.GetShopItemIDs()) > 0
}
func (t *Town) GetPosition() (int, int) {
@ -192,3 +193,19 @@ func (t *Town) SetPosition(x, y int) {
t.X = x
t.Y = y
}
func (t *Town) GetShopItems() ([]*items.Item, error) {
itemIDs := t.GetShopItemIDs()
result := make([]*items.Item, 0, len(itemIDs))
for _, itemID := range itemIDs {
item, err := items.Find(itemID)
if err != nil {
// Skip invalid item IDs rather than failing entirely
continue
}
result = append(result, item)
}
return result, nil
}

View File

@ -8,9 +8,9 @@ import (
"dk/internal/database"
"dk/internal/helpers"
"dk/internal/helpers/exp"
"dk/internal/models/classes"
"dk/internal/models/spells"
"dk/internal/models/towns"
)
// User represents a user in the game
@ -213,6 +213,10 @@ func (u *User) GetSpells() ([]*spells.Spell, error) {
return spells.UserSpells(u.ID)
}
func (u *User) GetHealingSpells() ([]*spells.Spell, error) {
return spells.UserHealingSpells(u.ID)
}
func (u *User) HasSpell(spellID int) bool {
return spells.HasSpell(u.ID, spellID)
}
@ -288,7 +292,7 @@ func (u *User) SetPosition(x, y int) {
}
func (u *User) ExpNeededForNextLevel() int {
return exp.Calc(u.Level + 1)
return helpers.ExpAtLevel(u.Level + 1)
}
func (u *User) GrantExp(expAmount int) map[string]any {
@ -324,7 +328,7 @@ func (u *User) CalculateLevelUp(expGain int) (newLevel, newStr, newDex, newExp i
totalExp := u.Exp + expGain
for {
expNeeded := exp.Calc(level + 1)
expNeeded := helpers.ExpAtLevel(level + 1)
if totalExp < expNeeded {
break
}
@ -343,7 +347,7 @@ func (u *User) ExpProgress() float64 {
return float64(u.Exp) / float64(u.ExpNeededForNextLevel()) * 100
}
currentLevelExp := exp.Calc(u.Level)
currentLevelExp := helpers.ExpAtLevel(u.Level)
nextLevelExp := u.ExpNeededForNextLevel()
progressExp := u.Exp
@ -375,3 +379,19 @@ func (u *User) learnSpellsForLevel(level int) error {
return u.GrantSpells(spellIDs)
}
func (u *User) GetKnownTowns() ([]*towns.Town, error) {
townIDs := u.GetTownIDs()
result := make([]*towns.Town, 0, len(townIDs))
for _, townID := range townIDs {
town, err := towns.Find(townID)
if err != nil {
// Skip invalid town IDs rather than failing entirely
continue
}
result = append(result, town)
}
return result, nil
}

View File

@ -9,7 +9,6 @@ import (
"dk/internal/models/towns"
"dk/internal/models/users"
"fmt"
"slices"
sushi "git.sharkk.net/Sharkk/Sushi"
"git.sharkk.net/Sharkk/Sushi/auth"
@ -131,20 +130,14 @@ func showShop(ctx sushi.Ctx) {
town := ctx.UserValue("town").(*towns.Town)
var itemlist []*items.Item
if town.ShopList != "" {
itemMap := helpers.NewOrderedMap[int, *items.Item]()
for _, id := range town.GetShopItems() {
if item, err := items.Find(id); err == nil {
itemMap.Set(id, item)
}
}
itemlist = itemMap.ToSlice()
itemList, err := town.GetShopItems()
if err != nil {
fmt.Printf("Town shop list error: %s\n", err.Error())
}
components.RenderPage(ctx, town.Name+" Shop", "town/shop.html", map[string]any{
"town": town,
"itemlist": itemlist,
"itemlist": itemList,
"error_message": errorHTML,
})
}
@ -155,7 +148,7 @@ func buyItem(ctx sushi.Ctx) {
id := ctx.Param("id").Int()
town := ctx.UserValue("town").(*towns.Town)
if !slices.Contains(town.GetShopItems(), id) {
if !town.HasShopItem(id) {
sess.SetFlash("error", "The item doesn't exist in this shop.")
ctx.Redirect("/town/shop")
return
@ -216,36 +209,12 @@ func showMaps(ctx sushi.Ctx) {
}
town := ctx.UserValue("town").(*towns.Town)
user := ctx.GetCurrentUser().(*users.User)
maplist := helpers.NewOrderedMap[int, Map]()
towns, _ := towns.All()
var mapped []int
if user.Towns != "" {
mapped = user.GetTownIDs()
}
for _, town := range towns {
var owned bool
if mapped != nil && slices.Contains(mapped, town.ID) {
owned = true
} else {
owned = false
}
maplist.Set(town.ID, Map{
ID: town.ID,
Name: town.Name,
Cost: town.MapCost,
Owned: owned,
X: town.X,
Y: town.Y,
TP: town.TPCost,
})
}
components.RenderPage(ctx, town.Name+" Maps", "town/maps.html", map[string]any{
"town": town,
"maplist": maplist.ToSlice(),
"towns": towns,
"error_message": errorHTML,
})
}

View File

@ -13,6 +13,7 @@ import (
"dk/internal/control"
"dk/internal/database"
"dk/internal/helpers/email"
"dk/internal/helpers/markdown"
"dk/internal/models/users"
"dk/internal/routes"
@ -142,10 +143,14 @@ func start(port string) error {
control.Init(filepath.Join(cwd, "data/control.json"))
defer control.Save()
controls := control.Get()
template.InitializeCache(cwd)
template.RegisterFunc("md", markdown.MarkdownTemplateFunc)
email.Init(controls.EmailMode, controls.EmailFilePath,
controls.SMTPHost, controls.SMTPPort, controls.SMTPUsername, controls.SMTPPassword)
authMW := auth.New(getUserByID)
app := sushi.New()

View File

@ -29,15 +29,17 @@
<h5>Town Maps</h5>
{if #_towns > 0}
{for t in _towns}
{if user.HasTownMap(t.ID)}
{if town != nil and t.Name == town.Name}
<span>{t.Name} <i>(here)</i></span>
{else}
{if user.TP < t.TPCost}
<span class="light">{t.Name}</span>
{else}
<a class="teleport-link" href="/teleport/{t.ID}" data-town="{t.Name}" data-cost="{t.TPCost}">{t.Name}</a>
<a class="teleport-link" href="/teleport/{t.ID}" data-town="{t.Name}" data-cost="{t.TPCost}">{t.Name}</a>
{/if}
{/if}
{/if}
{/for}
{else}
<i>No town maps</i>

View File

@ -76,8 +76,8 @@
<section>
<h5>Spells</h5>
{if #_spells > 0}
{for id,name in _spells}
<a href="#">{name}</a>
{for spell in _spells}
<a href="#">{spell.Name}</a>
{/for}
{else}
<i>No known healing spells</i>