fix model saving, add resting

This commit is contained in:
Sky Johnson 2025-08-12 17:19:44 -05:00
parent 56aa3afd4f
commit e3a125f6cf
5 changed files with 137 additions and 20 deletions

View File

@ -320,4 +320,8 @@ div#statbars {
}
}
}
}
.inline-block {
display: inline-block;
}

View File

@ -27,12 +27,12 @@ type BaseModel struct {
// Set uses reflection to set a field and track changes
func Set(model Model, field string, value any) error {
v := reflect.ValueOf(model).Elem()
fieldVal := v.FieldByName(field)
t := v.Type()
fieldVal := v.FieldByName(field)
if !fieldVal.IsValid() {
return fmt.Errorf("field %s does not exist", field)
}
if !fieldVal.CanSet() {
return fmt.Errorf("field %s cannot be set", field)
}
@ -42,13 +42,17 @@ func Set(model Model, field string, value any) error {
// Only set if value has changed
if !reflect.DeepEqual(currentVal, value) {
// Convert value to correct type
newVal := reflect.ValueOf(value)
if newVal.Type().ConvertibleTo(fieldVal.Type()) {
fieldVal.Set(newVal.Convert(fieldVal.Type()))
// Convert field name to snake_case for database
dbField := toSnakeCase(field)
// Get db column name from struct tag
structField, _ := t.FieldByName(field)
dbField := structField.Tag.Get("db")
if dbField == "" {
dbField = toSnakeCase(field) // fallback
}
model.SetDirty(dbField, value)
} else {
return fmt.Errorf("cannot convert %T to %s", value, fieldVal.Type())
@ -63,7 +67,10 @@ func toSnakeCase(s string) string {
var result strings.Builder
for i, r := range s {
if i > 0 && r >= 'A' && r <= 'Z' {
result.WriteByte('_')
prev := rune(s[i-1])
if prev < 'A' || prev > 'Z' {
result.WriteByte('_')
}
}
if r >= 'A' && r <= 'Z' {
result.WriteRune(r - 'A' + 'a')

View File

@ -1,10 +1,13 @@
package routes
import (
"dk/internal/auth"
"dk/internal/middleware"
"dk/internal/router"
"dk/internal/template/components"
"dk/internal/towns"
"dk/internal/users"
"fmt"
)
func RegisterTownRoutes(r *router.Router) {
@ -14,6 +17,7 @@ func RegisterTownRoutes(r *router.Router) {
group.Get("/", showTown)
group.Get("/inn", showInn)
group.WithMiddleware(middleware.CSRF(auth.Manager)).Post("/inn", rest)
}
func showTown(ctx router.Ctx, _ []string) {
@ -28,6 +32,29 @@ func showTown(ctx router.Ctx, _ []string) {
func showInn(ctx router.Ctx, _ []string) {
town := ctx.UserValue("town").(*towns.Town)
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
"town": town,
"town": town,
"rested": false,
})
}
func rest(ctx router.Ctx, _ []string) {
town := ctx.UserValue("town").(*towns.Town)
user := ctx.UserValue("user").(*users.User)
if user.Gold < town.InnCost {
fmt.Println("Cant afford")
ctx.Redirect("/town", 303)
return
}
user.Set("Gold", user.Gold-town.InnCost)
user.Set("HP", user.MaxHP)
user.Set("MP", user.MaxMP)
user.Set("TP", user.MaxTP)
user.Save()
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
"town": town,
"rested": true,
})
}

View File

@ -557,17 +557,41 @@ func (t *Template) processConditionals(content string, data map[string]any) stri
condition := strings.TrimSpace(result[start+4 : headerEnd]) // Skip "{if "
contentStart := headerEnd + 1
endTag := "{/if}"
contentEnd := strings.Index(result[contentStart:], endTag)
// Find matching {/if} by tracking nesting level
nestLevel := 1
pos := contentStart
contentEnd := -1
for pos < len(result) && nestLevel > 0 {
ifStart := strings.Index(result[pos:], "{if ")
endStart := strings.Index(result[pos:], "{/if}")
if ifStart != -1 && (endStart == -1 || ifStart < endStart) {
// Found nested {if}
nestLevel++
pos += ifStart + 4
} else if endStart != -1 {
// Found {/if}
nestLevel--
if nestLevel == 0 {
contentEnd = pos + endStart
break
}
pos += endStart + 5
} else {
break
}
}
if contentEnd == -1 {
break
}
contentEnd += contentStart
ifContent := result[contentStart:contentEnd]
// Check for else clause
elseStart := strings.Index(ifContent, "{else}")
// Check for else clause at the same nesting level
elseStart := t.findElseAtLevel(ifContent)
var trueContent, falseContent string
if elseStart != -1 {
trueContent = ifContent[:elseStart]
@ -588,12 +612,61 @@ func (t *Template) processConditionals(content string, data map[string]any) stri
selectedContent = t.processLoops(selectedContent, data)
selectedContent = t.processConditionals(selectedContent, data)
result = result[:start] + selectedContent + result[contentEnd+len(endTag):]
result = result[:start] + selectedContent + result[contentEnd+5:] // +5 for "{/if}"
}
return result
}
// findElseAtLevel finds {else} at the top level (not nested)
func (t *Template) findElseAtLevel(content string) int {
nestLevel := 0
pos := 0
for pos < len(content) {
ifStart := strings.Index(content[pos:], "{if ")
elseStart := strings.Index(content[pos:], "{else}")
endStart := strings.Index(content[pos:], "{/if}")
// Find the earliest occurrence
earliest := -1
var tag string
if ifStart != -1 && (earliest == -1 || ifStart < earliest-pos) {
earliest = pos + ifStart
tag = "if"
}
if elseStart != -1 && (earliest == -1 || elseStart < earliest-pos) {
earliest = pos + elseStart
tag = "else"
}
if endStart != -1 && (earliest == -1 || endStart < earliest-pos) {
earliest = pos + endStart
tag = "end"
}
if earliest == -1 {
break
}
switch tag {
case "if":
nestLevel++
pos = earliest + 4
case "else":
if nestLevel == 0 {
return earliest
}
pos = earliest + 6
case "end":
nestLevel--
pos = earliest + 5
}
}
return -1
}
// evaluateCondition evaluates simple conditions like "user.name", "count > 0", "items"
func (t *Template) evaluateCondition(condition string, data map[string]any) bool {
condition = strings.TrimSpace(condition)

View File

@ -4,16 +4,22 @@
<div class="town inn">
<div class="title"><h3>{town.Name} Inn</h3></div>
{if user.Gold < town.InnCost}
<p>You do not have enough gold to stay at this Inn tonight.</p>
{if rested}
<p>You wake up feeling refreshed and ready for action!</p>
<p>You may return to <a href="/town">town</a>, or use the compass on the left to explore.</p>
{else}
Resting at the inn will refill your current HP, MP, and TP to their maximum levels.<br><br>
A night's sleep at this Inn will cost you <b>{town.InnCost} gold</b>. Is that ok?<br><br>
<form action="/town/inn" method="post">
<button class="btn" type="submit">Yes</button>
{if user.Gold < town.InnCost}
<p>You do not have enough gold to stay at this inn tonight.</p>
<p>You may return to <a href="/town">town</a>, or use the compass on the left to explore.</p>
{else}
Resting at the inn will refill your current HP, MP, and TP to their maximum levels.<br><br>
A night's sleep at this inn will cost you <b>{town.InnCost} gold</b>. Is that ok?<br><br>
<form action="/town/inn" method="post" class="inline-block">
{csrf}
<button class="btn" type="submit">Yes</button>
</form>
<a href="/town"><button class="btn">No</button></a>
</form>
{/if}
{/if}
</div>
{/block}