10 Commits

18 changed files with 201 additions and 32 deletions

4
.gitignore vendored
View File

@ -10,4 +10,6 @@ goonscape.exe
# OS files
.DS_Store
Thumbs.db
Thumbs.db
resources/models/old_and_test/

24
LICENSE
View File

@ -1,11 +1,21 @@
“Commons Clause” License Condition v1.0
MIT License
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition.
Copyright (c) 2025 bdnugget
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, right to Sell the Software.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Cause License Condition notice.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Software: GoonScape
License: Commons Clause v1.0
Licensor: bdnugget
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -5,22 +5,79 @@ import (
rl "github.com/gen2brain/raylib-go/raylib"
)
// Helper function to load animations for a model
func loadModelAnimations(animPaths map[string]string) (types.AnimationSet, error) {
var animSet types.AnimationSet
// Load idle animations if specified
if idlePath, ok := animPaths["idle"]; ok {
idleAnims := rl.LoadModelAnimations(idlePath)
if len(idleAnims) > 0 {
animSet.Idle = idleAnims
rl.TraceLog(rl.LogInfo, "Loaded idle animation: %s (%d frames, %f seconds)",
idlePath, idleAnims[0].FrameCount, float32(idleAnims[0].FrameCount)/60.0)
}
}
// Load walk animations if specified
if walkPath, ok := animPaths["walk"]; ok {
walkAnims := rl.LoadModelAnimations(walkPath)
if len(walkAnims) > 0 {
animSet.Walk = walkAnims
rl.TraceLog(rl.LogInfo, "Loaded walk animation: %s (%d frames, %f seconds)",
walkPath, walkAnims[0].FrameCount, float32(walkAnims[0].FrameCount)/60.0)
}
}
return animSet, nil
}
func LoadModels() ([]types.ModelAsset, error) {
goonerModel := rl.LoadModel("resources/models/goonion.obj")
goonerTexture := rl.LoadTexture("resources/models/goonion.png")
rl.SetMaterialTexture(goonerModel.Materials, rl.MapDiffuse, goonerTexture)
// Goonion model and animations
goonerModel := rl.LoadModel("resources/models/gooner/walk_no_y_transform.glb")
goonerAnims, _ := loadModelAnimations(map[string]string{"idle": "resources/models/gooner/idle_no_y_transform.glb", "walk": "resources/models/gooner/walk_no_y_transform.glb"})
coomerModel := rl.LoadModel("resources/models/coomer.obj")
coomerTexture := rl.LoadTexture("resources/models/coomer.png")
rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)
// Apply transformations
transform := rl.MatrixIdentity()
transform = rl.MatrixMultiply(transform, rl.MatrixRotateY(180*rl.Deg2rad))
transform = rl.MatrixMultiply(transform, rl.MatrixRotateX(-90*rl.Deg2rad))
transform = rl.MatrixMultiply(transform, rl.MatrixScale(1.0, 1.0, 1.0))
goonerModel.Transform = transform
// Coomer model (ready for animations)
coomerModel := rl.LoadModel("resources/models/coomer/idle_notransy.glb")
// coomerTexture := rl.LoadTexture("resources/models/coomer.png")
// rl.SetMaterialTexture(coomerModel.Materials, rl.MapDiffuse, coomerTexture)
// When you have animations, add them like:
coomerAnims, _ := loadModelAnimations(map[string]string{"idle": "resources/models/coomer/idle_notransy.glb", "walk": "resources/models/coomer/unsteadywalk_notransy.glb"})
coomerModel.Transform = transform
// Shreke model (ready for animations)
shrekeModel := rl.LoadModel("resources/models/shreke.obj")
shrekeTexture := rl.LoadTexture("resources/models/shreke.png")
rl.SetMaterialTexture(shrekeModel.Materials, rl.MapDiffuse, shrekeTexture)
// When you have animations, add them like:
// shrekeAnims, _ := loadModelAnimations("resources/models/shreke.glb",
// map[string]string{
// "idle": "resources/models/shreke_idle.glb",
// "walk": "resources/models/shreke_walk.glb",
// })
return []types.ModelAsset{
{Model: goonerModel, Texture: goonerTexture},
{Model: coomerModel, Texture: coomerTexture},
{
Model: goonerModel,
Animation: append(goonerAnims.Idle, goonerAnims.Walk...),
AnimFrames: int32(len(goonerAnims.Idle) + len(goonerAnims.Walk)),
Animations: goonerAnims,
YOffset: 0.0,
},
{
Model: coomerModel,
Animation: append(coomerAnims.Idle, coomerAnims.Walk...),
AnimFrames: int32(len(coomerAnims.Idle) + len(coomerAnims.Walk)),
Animations: coomerAnims,
YOffset: -4.0,
},
{Model: shrekeModel, Texture: shrekeTexture},
}, nil
}
@ -31,6 +88,11 @@ func LoadMusic(filename string) (rl.Music, error) {
func UnloadModels(models []types.ModelAsset) {
for _, model := range models {
if model.Animation != nil {
for i := int32(0); i < model.AnimFrames; i++ {
rl.UnloadModelAnimation(model.Animation[i])
}
}
rl.UnloadModel(model.Model)
rl.UnloadTexture(model.Texture)
}

View File

@ -10,6 +10,7 @@ var (
cameraDistance = float32(20.0)
cameraYaw = float32(145.0)
cameraPitch = float32(45.0)
lastMousePos rl.Vector2 // Add this to track mouse movement
)
func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
@ -32,6 +33,34 @@ func UpdateCamera(camera *rl.Camera3D, player rl.Vector3, deltaTime float32) {
}
}
// Handle middle mouse camera rotation
if rl.IsMouseButtonDown(rl.MouseMiddleButton) {
currentMousePos := rl.GetMousePosition()
// If we just started holding the button, initialize last position
if !rl.IsMouseButtonPressed(rl.MouseMiddleButton) {
mouseDelta := rl.Vector2{
X: currentMousePos.X - lastMousePos.X,
Y: currentMousePos.Y - lastMousePos.Y,
}
// Adjust rotation speed as needed
cameraYaw += mouseDelta.X * 0.5 * deltaTime * 60
cameraPitch += mouseDelta.Y * 0.5 * deltaTime * 60
// Clamp pitch to prevent camera flipping
if cameraPitch < 20 {
cameraPitch = 20
}
if cameraPitch > 85 {
cameraPitch = 85
}
}
lastMousePos = currentMousePos
}
// Keep the keyboard controls too
if rl.IsKeyDown(rl.KeyRight) {
cameraYaw += 100 * deltaTime
}

View File

@ -68,17 +68,14 @@ func (g *Game) Update(deltaTime float32) {
return
}
// Assign model based on player ID
modelIndex := int(playerID) % len(g.Models)
g.Player = &types.Player{
Speed: 50.0,
TargetPath: []types.Tile{},
UserData: g,
QuitDone: make(chan struct{}),
ID: playerID,
Model: g.Models[modelIndex].Model,
Texture: g.Models[modelIndex].Texture,
}
g.AssignModelToPlayer(g.Player)
go network.HandleServerCommunication(conn, playerID, g.Player, g.OtherPlayers, g.QuitChan)
g.isLoggedIn = true
@ -157,14 +154,32 @@ func (g *Game) DrawPlayer(player *types.Player, model rl.Model) {
defer player.Unlock()
grid := GetMapGrid()
modelIndex := int(player.ID) % len(g.Models)
modelAsset := g.Models[modelIndex]
const defaultHeight = 8.0 // Default height above tile, fine tune per model in types.ModelAsset
playerPos := rl.Vector3{
X: player.PosActual.X,
Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + 16.0,
Y: grid[player.PosTile.X][player.PosTile.Y].Height*types.TileHeight + defaultHeight + modelAsset.YOffset,
Z: player.PosActual.Z,
}
// Check if model has animations
if modelAsset.Animations.Idle != nil || modelAsset.Animations.Walk != nil {
if player.IsMoving && len(modelAsset.Animations.Walk) > 0 {
anim := modelAsset.Animations.Walk[0] // Use first walk animation
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
rl.UpdateModelAnimation(model, anim, player.AnimationFrame)
} else if len(modelAsset.Animations.Idle) > 0 {
anim := modelAsset.Animations.Idle[0] // Use first idle animation
player.AnimationFrame = player.AnimationFrame % anim.FrameCount
rl.UpdateModelAnimation(model, anim, player.AnimationFrame)
}
}
rl.DrawModel(model, playerPos, 16, rl.White)
// Draw floating messages and path indicators
if player.FloatingMessage != nil {
screenPos := rl.GetWorldToScreen(rl.Vector3{
X: playerPos.X,
@ -207,12 +222,9 @@ func (g *Game) Render() {
rl.BeginMode3D(g.Camera)
g.DrawMap()
g.DrawPlayer(g.Player, g.Player.Model)
for id, other := range g.OtherPlayers {
for _, other := range g.OtherPlayers {
if other.Model.Meshes == nil {
// Assign model based on player ID for consistency
modelIndex := int(id) % len(g.Models)
other.Model = g.Models[modelIndex].Model
other.Texture = g.Models[modelIndex].Texture
g.AssignModelToPlayer(other)
}
g.DrawPlayer(other, other.Model)
}
@ -352,3 +364,12 @@ func (g *Game) Shutdown() {
func (g *Game) HandleServerMessages(messages []*pb.ChatMessage) {
g.Chat.HandleServerMessages(messages)
}
func (g *Game) AssignModelToPlayer(player *types.Player) {
modelIndex := int(player.ID) % len(g.Models)
modelAsset := g.Models[modelIndex]
// Just use the original model - don't try to copy it
player.Model = modelAsset.Model
player.Texture = modelAsset.Texture
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -19,6 +19,33 @@ func (p *Player) MoveTowards(target Tile, deltaTime float32, mapGrid [][]Tile) {
direction := rl.Vector3Subtract(targetPos, p.PosActual)
distance := rl.Vector3Length(direction)
if distance > 1.0 {
wasMoving := p.IsMoving
p.IsMoving = true
if !wasMoving {
p.AnimationFrame = 0
}
oldFrame := p.AnimationFrame
p.AnimationFrame += int32(deltaTime * 60)
rl.TraceLog(rl.LogInfo, "Walk frame update: %d -> %d (delta: %f)",
oldFrame, p.AnimationFrame, deltaTime)
} else {
wasMoving := p.IsMoving
p.IsMoving = false
if wasMoving {
p.AnimationFrame = 0
}
oldFrame := p.AnimationFrame
p.AnimationFrame += int32(deltaTime * 60)
rl.TraceLog(rl.LogInfo, "Idle frame update: %d -> %d (delta: %f)",
oldFrame, p.AnimationFrame, deltaTime)
}
if distance > 0 {
direction = rl.Vector3Scale(direction, p.Speed*deltaTime/distance)
}
@ -41,9 +68,12 @@ func NewPlayer(state *pb.PlayerState) *Player {
Y: float32(state.Y * TileHeight),
Z: float32(state.Y * TileSize),
},
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
Speed: 50.0,
ID: state.PlayerId,
PosTile: Tile{X: int(state.X), Y: int(state.Y)},
Speed: 50.0,
ID: state.PlayerId,
IsMoving: false,
AnimationFrame: 0,
LastAnimUpdate: time.Now(),
}
}

View File

@ -26,15 +26,30 @@ type Player struct {
ID int32
CurrentTick int64
LastUpdateTime time.Time
LastAnimUpdate time.Time
InterpolationProgress float32
UserData interface{}
FloatingMessage *FloatingMessage
QuitDone chan struct{}
AnimationFrame int32
IsMoving bool
}
type AnimationSet struct {
Idle []rl.ModelAnimation
Walk []rl.ModelAnimation
// Can add more animation types later like:
// Attack []ModelAnimation
// Jump []ModelAnimation
}
type ModelAsset struct {
Model rl.Model
Texture rl.Texture2D
Model rl.Model
Texture rl.Texture2D
Animation []rl.ModelAnimation // Keep this for compatibility
AnimFrames int32 // Keep this for compatibility
Animations AnimationSet // New field for organized animations
YOffset float32 // Additional height offset (added to default 8.0)
}
type ChatMessage struct {