diff --git a/assets/dk.css b/assets/dk.css index 266fb53..8a8a565 100644 --- a/assets/dk.css +++ b/assets/dk.css @@ -320,4 +320,8 @@ div#statbars { } } } +} + +.inline-block { + display: inline-block; } \ No newline at end of file diff --git a/internal/database/model.go b/internal/database/model.go index 692fae1..c1c483d 100644 --- a/internal/database/model.go +++ b/internal/database/model.go @@ -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') diff --git a/internal/routes/town.go b/internal/routes/town.go index ac77b00..aa4e3a5 100644 --- a/internal/routes/town.go +++ b/internal/routes/town.go @@ -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, }) } diff --git a/internal/template/template.go b/internal/template/template.go index 4334596..d283fb5 100644 --- a/internal/template/template.go +++ b/internal/template/template.go @@ -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) diff --git a/templates/town/inn.html b/templates/town/inn.html index d2bf41c..eefeeaf 100644 --- a/templates/town/inn.html +++ b/templates/town/inn.html @@ -4,16 +4,22 @@

{town.Name} Inn

- {if user.Gold < town.InnCost} -

You do not have enough gold to stay at this Inn tonight.

+ {if rested} +

You wake up feeling refreshed and ready for action!

You may return to town, or use the compass on the left to explore.

{else} - Resting at the inn will refill your current HP, MP, and TP to their maximum levels.

- A night's sleep at this Inn will cost you {town.InnCost} gold. Is that ok?

-
- + {if user.Gold < town.InnCost} +

You do not have enough gold to stay at this inn tonight.

+

You may return to town, or use the compass on the left to explore.

+ {else} + Resting at the inn will refill your current HP, MP, and TP to their maximum levels.

+ A night's sleep at this inn will cost you {town.InnCost} gold. Is that ok?

+ + {csrf} + +
- + {/if} {/if}
{/block} \ No newline at end of file