create fights, disable compass

This commit is contained in:
Sky Johnson 2025-08-14 19:05:35 -05:00
parent 3fd28e0cd9
commit 674cfce506
12 changed files with 282 additions and 38 deletions

View File

@ -395,3 +395,10 @@ div.modal {
.mt-1 {
margin-top: 1rem;
}
img#move-compass-disabled {
margin: 0px auto;
cursor: not-allowed;
filter: grayscale();
margin-top: 0.5rem;
}

65
data/fights.json Normal file
View File

@ -0,0 +1,65 @@
[
{
"id": 1,
"user_id": 1,
"monster_id": 10,
"monster_hp": 16,
"monster_max_hp": 16,
"monster_sleep": 0,
"monster_immune": 0,
"uber_damage": 0,
"uber_defense": 0,
"first_strike": false,
"turn": 1,
"ran_away": false,
"victory": false,
"won": false,
"reward_gold": 0,
"reward_exp": 0,
"actions": [],
"created": 1755215734,
"updated": 1755215734
},
{
"id": 2,
"user_id": 1,
"monster_id": 7,
"monster_hp": 12,
"monster_max_hp": 12,
"monster_sleep": 0,
"monster_immune": 1,
"uber_damage": 0,
"uber_defense": 0,
"first_strike": true,
"turn": 1,
"ran_away": false,
"victory": false,
"won": false,
"reward_gold": 0,
"reward_exp": 0,
"actions": [],
"created": 1755215776,
"updated": 1755215776
},
{
"id": 3,
"user_id": 1,
"monster_id": 8,
"monster_hp": 14,
"monster_max_hp": 14,
"monster_sleep": 0,
"monster_immune": 0,
"uber_damage": 0,
"uber_defense": 0,
"first_strike": true,
"turn": 1,
"ran_away": false,
"victory": false,
"won": false,
"reward_gold": 0,
"reward_exp": 0,
"actions": [],
"created": 1755215777,
"updated": 1755215777
}
]

View File

@ -2,9 +2,13 @@ package actions
import (
"dk/internal/models/control"
"dk/internal/models/fights"
"dk/internal/models/monsters"
"dk/internal/models/towns"
"dk/internal/models/users"
"fmt"
"math"
"math/rand"
)
type Direction int
@ -54,5 +58,67 @@ func Move(user *users.User, dir Direction) (string, int, int, error) {
return "In Town", newX, newY, nil
}
// 33% chance to start a fight when moving to non-town location
if rand.Float32() < 0.33 {
fmt.Println("Fight chance!")
monster, err := getRandomMonsterByDistance(newX, newY)
if err != nil {
fmt.Printf("Finding a monster failed: %s\n", err.Error())
return "Exploring", newX, newY, nil // Fall back to exploring if monster selection fails
}
// Create new fight record
fight := fights.New(user.ID, monster.ID)
fight.MonsterHP = monster.MaxHP
fight.MonsterMaxHP = monster.MaxHP
fight.MonsterImmune = monster.Immune
err = fight.Insert()
if err != nil {
fmt.Printf("Inserting a fight failed: %s\n", err.Error())
return "Exploring", newX, newY, nil // Fall back if fight creation fails
}
user.FightID = fight.ID
return "Fighting", newX, newY, nil
} else {
fmt.Println("No fight...")
}
return "Exploring", newX, newY, nil
}
// getRandomMonsterByDistance selects a random monster based on distance from origin
func getRandomMonsterByDistance(x, y int) (*monsters.Monster, error) {
// Calculate Manhattan distance from origin
distance := int(math.Abs(float64(x)) + math.Abs(float64(y)))
// Determine level range based on distance (every 5 units increases level)
minLevel := 1 + (distance / 5)
maxLevel := minLevel + 2 // 3-level range
// Cap the levels at reasonable bounds
if minLevel > 50 {
minLevel = 50
}
if maxLevel > 50 {
maxLevel = 50
}
if minLevel < 1 {
minLevel = 1
}
// Get monsters in the level range
availableMonsters, err := monsters.ByLevelRange(minLevel, maxLevel)
if err != nil || len(availableMonsters) == 0 {
// Fallback to level 1 monsters if no monsters found
availableMonsters, err = monsters.ByLevel(1)
if err != nil || len(availableMonsters) == 0 {
return nil, fmt.Errorf("no monsters available")
}
}
// Pick a random monster from available ones
randomIndex := rand.Intn(len(availableMonsters))
return availableMonsters[randomIndex], nil
}

View File

@ -0,0 +1,59 @@
package middleware
import (
"dk/internal/models/users"
"dk/internal/router"
"strings"
"github.com/valyala/fasthttp"
)
// RequireFighting ensures the user is in a fight when accessing fight routes
func RequireFighting() router.Middleware {
return func(next router.Handler) router.Handler {
return func(ctx router.Ctx, params []string) {
user, ok := ctx.UserValue("user").(*users.User)
if !ok || user == nil {
ctx.SetStatusCode(fasthttp.StatusUnauthorized)
ctx.SetBodyString("Not authenticated")
return
}
if !user.IsFighting() {
ctx.Redirect("/", 303)
return
}
next(ctx, params)
}
}
}
// HandleFightRedirect redirects users to appropriate locations based on fight status
func HandleFightRedirect() router.Middleware {
return func(next router.Handler) router.Handler {
return func(ctx router.Ctx, params []string) {
user, ok := ctx.UserValue("user").(*users.User)
if !ok || user == nil {
next(ctx, params)
return
}
currentPath := string(ctx.URI().Path())
// If user is fighting and not on fight page, redirect to fight
if user.IsFighting() && !strings.HasPrefix(currentPath, "/fight") {
ctx.Redirect("/fight", 303)
return
}
// If user is not fighting and on fight page, redirect to home
if !user.IsFighting() && strings.HasPrefix(currentPath, "/fight") {
ctx.Redirect("/", 303)
return
}
next(ctx, params)
}
}
}

View File

@ -51,7 +51,7 @@ func New(userID, monsterID int) *Fight {
UberDamage: 0,
UberDefense: 0,
FirstStrike: false,
Turn: 1,
Turn: 0,
RanAway: false,
Victory: false,
Won: false,
@ -71,8 +71,8 @@ func (f *Fight) Validate() error {
if f.MonsterID <= 0 {
return fmt.Errorf("fight MonsterID must be positive")
}
if f.Turn < 1 {
return fmt.Errorf("fight Turn must be at least 1")
if f.Turn < 0 {
return fmt.Errorf("fight Turn cannot be negative")
}
if f.MonsterHP < 0 {
return fmt.Errorf("fight MonsterHP cannot be negative")

View File

@ -33,15 +33,8 @@ func RegisterAuthRoutes(r *router.Router) {
// showLogin displays the login form
func showLogin(ctx router.Ctx, _ []string) {
sess := ctx.UserValue("session").(*session.Session)
var errorHTML string
var id string
if flash, exists := sess.GetFlash("error"); exists {
if msg, ok := flash.(string); ok {
errorHTML = fmt.Sprintf(`<div style="color: red; margin-bottom: 1rem;">%s</div>`, msg)
}
}
if formData, exists := sess.Get("form_data"); exists {
if data, ok := formData.(map[string]string); ok {
id = data["id"]
@ -51,8 +44,7 @@ func showLogin(ctx router.Ctx, _ []string) {
session.Store(sess)
components.RenderPage(ctx, "Log In", "auth/login.html", map[string]any{
"error_message": errorHTML,
"id": id,
"id": id,
})
}

43
internal/routes/fight.go Normal file
View File

@ -0,0 +1,43 @@
package routes
import (
"dk/internal/auth"
"dk/internal/components"
"dk/internal/middleware"
"dk/internal/models/fights"
"dk/internal/models/users"
"dk/internal/router"
"math/rand"
)
func RegisterFightRoutes(r *router.Router) {
group := r.Group("/fight")
group.Use(auth.RequireAuth())
group.Use(middleware.RequireFighting())
group.Get("/", showFight)
}
func showFight(ctx router.Ctx, _ []string) {
user := ctx.UserValue("user").(*users.User)
fight, err := fights.Find(user.FightID)
if err != nil {
ctx.SetContentType("text/plain")
ctx.SetBodyString("Fight not found")
return
}
// If turn 0, determine first strike and advance to turn 1
if fight.Turn == 0 {
// 50% chance user goes first
fight.FirstStrike = rand.Float32() < 0.5
fight.Turn = 1
fight.Save()
}
components.RenderPage(ctx, "Fighting", "fight/fight.html", map[string]any{
"fight": fight,
"user": user,
})
}

View File

@ -23,13 +23,23 @@ func Index(ctx router.Ctx, _ []string) {
ctx.Redirect("/town", 303)
case "Exploring":
ctx.Redirect("/explore", 303)
case "Fighting":
ctx.Redirect("/fight", 303)
default:
ctx.Redirect("/explore", 303)
}
}
func Move(ctx router.Ctx, _ []string) {
sess := ctx.UserValue("session").(*session.Session)
user := ctx.UserValue("user").(*users.User)
if user.Currently == "Fighting" {
sess.SetFlash("error", "You can't just run from a fight!")
ctx.Redirect("/fight", 303)
return
}
dir, err := strconv.Atoi(string(ctx.PostArgs().Peek("direction")))
if err != nil {
ctx.SetContentType("text/plain")
@ -46,13 +56,16 @@ func Move(ctx router.Ctx, _ []string) {
user.Currently = currently
user.SetPosition(newX, newY)
user.Save()
if currently == "In Town" {
switch currently {
case "In Town":
ctx.Redirect("/town", 303)
return
case "Fighting":
ctx.Redirect("/fight", 303)
default:
ctx.Redirect("/explore", 303)
}
ctx.Redirect("/explore", 303)
}
func Explore(ctx router.Ctx, _ []string) {

View File

@ -50,21 +50,10 @@ func showTown(ctx router.Ctx, _ []string) {
}
func showInn(ctx router.Ctx, _ []string) {
sess := ctx.UserValue("session").(*session.Session)
var errorHTML string
if flash, exists := sess.GetFlash("error"); exists {
if msg, ok := flash.(string); ok {
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + msg + "</div>"
}
}
town := ctx.UserValue("town").(*towns.Town)
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
"town": town,
"rested": false,
"error_message": errorHTML,
"town": town,
"rested": false,
})
}

View File

@ -189,6 +189,7 @@ func start(port string) error {
routes.RegisterAuthRoutes(r)
routes.RegisterTownRoutes(r)
routes.RegisterFightRoutes(r)
// Use current working directory for static files
assetsDir := filepath.Join(cwd, "assets")

View File

@ -0,0 +1,5 @@
{include "layout.html"}
{block "content"}
ITS A FIGHT!
{/block}

View File

@ -12,15 +12,19 @@
<a href="javascript:open_map_popup()">View Map</a>
<form id="move-compass" action="/move" method="post" >
{csrf}
<button id="north" name="direction" value="0">North</button>
<div class="mid">
<button id="west" name="direction" value="3">West</button>
<button id="east" name="direction" value="1">East</button>
</div>
<button id="south" name="direction" value="2">South</button>
</form>
{if user.Currently != "Fighting"}
<form id="move-compass" action="/move" method="post" >
{csrf}
<button id="north" name="direction" value="0">North</button>
<div class="mid">
<button id="west" name="direction" value="3">West</button>
<button id="east" name="direction" value="1">East</button>
</div>
<button id="south" name="direction" value="2">South</button>
</form>
{else}
<img id="move-compass-disabled" src="/assets/images/compass.webp" alt="Can't run!" title="Can't run!">
{/if}
</section>
<section>