diff --git a/character/character.go b/character/character.go new file mode 100644 index 0000000..06032cb --- /dev/null +++ b/character/character.go @@ -0,0 +1,51 @@ +package character + +import ( + "image/color" + p "loading/point" + "math" + "math/rand" + "strconv" +) + +const ( + agentIdMax = 100 + offset = 15 + xd = offset + yd = offset +) + +type Character struct { + Name string + Pose p.Point + Shape []p.Point + Color color.RGBA + Angle float32 + DeltaAngle float32 + SignFlip float32 + Buffer float32 +} + +func NewCharacter() Character { + return Character{ + Name: "Agent-" + strconv.Itoa(rand.Intn(agentIdMax)), + Pose: p.Point{X: 0, Y: 0}, + Shape: []p.Point{{X: 0, Y: 0}, {X: xd, Y: 0}, {X: xd, Y: yd}}, + Color: color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}, + Angle: (rand.Float32() * 2 * math.Pi), + DeltaAngle: 0, + Buffer: offset, + } + +} + +func (c *Character) SetPose(x float32, y float32) { + c.Pose.X = x + c.Pose.Y = y +} + +func (c *Character) LoadShape(vertices []p.Point) { + + c.Shape = c.Shape[:0] + c.Shape = append(c.Shape, vertices...) +} diff --git a/drawer.go b/drawer.go new file mode 100644 index 0000000..f939ed0 --- /dev/null +++ b/drawer.go @@ -0,0 +1,147 @@ +package main + +import ( + "fmt" + "image/color" + "loading/character" + "loading/objects" + "math" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/text" + "github.com/hajimehoshi/ebiten/v2/vector" +) + +func DrawBobble(b objects.Bobble, image *ebiten.Image) { + + if b.Radius < 0 { + return + } + vector.DrawFilledCircle(image, b.Pose.X, b.Pose.Y, b.Radius+10, b.Bordercolor, true) + vector.DrawFilledCircle(image, b.Pose.X, b.Pose.Y, b.Radius, b.Color, true) +} + +func DrawCharacter(c character.Character, image *ebiten.Image) { + + if len(c.Shape) <= 0 { + return + } + + var path vector.Path + path.MoveTo(c.Pose.X, c.Pose.Y) + for _, p := range c.Shape { + + //rotation time *dance* + newX := c.Pose.X + float32(math.Cos(float64(c.Angle)))*(p.X) - float32(math.Sin(float64(c.Angle)))*(p.Y) + newY := c.Pose.Y + float32(math.Sin(float64(c.Angle)))*(p.X) + float32(math.Cos(float64(c.Angle)))*(p.Y) + path.LineTo(newX, newY) + } + path.Close() + + var vs []ebiten.Vertex + var is []uint16 + + vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) + + for i := range vs { + vs[i].SrcX = 1 + vs[i].SrcY = 1 + vs[i].ColorR = float32(c.Color.R) / 0xFF + vs[i].ColorG = float32(c.Color.B) / 0xFF + vs[i].ColorB = float32(c.Color.G) / 0xFF + vs[i].ColorA = 1 + } + + op := &ebiten.DrawTrianglesOptions{} + + //pose information + //angle := fmt.Sprintf("%.2f", c.Angle*180/math.Pi) + //delta := fmt.Sprintf("%.2f", c.DeltaAngle*180/math.Pi) + + //text.Draw(image, angle, mplusTinyFont, int(c.Pose.X+20), int(c.Pose.Y), color.White) + //text.Draw(image, delta, mplusTinyFont, int(c.Pose.X+20), int(c.Pose.Y+15), color.White) + + image.DrawTriangles(vs, is, whiteSubImage, op) +} + +func (g *Game) DrawLoading(image *ebiten.Image) { + + //identifier + text.Draw(image, versionInfo, mplusTinyFont, 20, 60, color.White) + + //radial + var xs []int + var ys []int + var tc []uint8 + var scales []float32 + + fade_scale := 0xFF / numTiles + + for i := 0; i < numTiles; i++ { + xs = append(xs, int(math.Cos(float64(g.counter)/(math.Pi*3)-float64(i)*math.Pi/3)*40+screenWidth/2-float64(tileWidth)/2)) + ys = append(ys, int(math.Sin(float64(g.counter)/(math.Pi*3)-float64(i)*math.Pi/3)*40+screenHeight/2-float64(tileHeight)/2)) + tc = append(tc, uint8(0xFF-fade_scale*i)) + + scales = append(scales, float32(math.Sin(float64(g.counter)/(math.Pi*12))/2+1)) + } + + for i := numTiles - 1; i >= 0; i-- { + drawTile(image, float32(xs[i]), float32(ys[i]), scales[i], color.RGBA{tc[i], tc[i], tc[i], 0xFF}) + } + + //Percentage >:] + //percent := (maxPercent - 1/math.Exp(float64(g.counter-1)-10/6)) / maxPercent + offset := float64(4.605) + exppart0 := float64(g.counter-1) / 10000 + + percent := maxPercent - 1/math.Exp(exppart0-offset) + if percent < 0 { + percent = 0 + } + + //100-1/e^(x/1000 - 4.6) + + text.Draw(image, fmt.Sprintf("%.2f%%", percent), mplusTinyFont, screenWidth/2-70/2, screenHeight-80, color.RGBA{0xC0, 0xC0, 0xC0, 0xC0}) +} + +func drawTile(screen *ebiten.Image, x float32, y float32, scale float32, c color.RGBA) { + var scaleFactor float32 = 1 + if scale > 0 { + scaleFactor = scale + } + drawTileToScale(screen, x, y, scaleFactor, c) +} + +func drawTileToScale(screen *ebiten.Image, x float32, y float32, scale float32, c color.RGBA) { + + var path vector.Path + + //scale delta + xd := tileWidth * scale + yd := tileHeight * scale + + //position at object origin shifted slightly for the half width/height adjustment + path.MoveTo(x-xd/2, y-yd/2) + path.LineTo(x+xd/2, y-yd/2) + path.LineTo(x+xd/2, y+yd/2) + path.LineTo(x-xd/2, y+yd/2) + path.LineTo(x-xd/2, y+yd/2) + path.Close() + + var vs []ebiten.Vertex + var is []uint16 + + vs, is = path.AppendVerticesAndIndicesForFilling(nil, nil) + + for i := range vs { + vs[i].SrcX = 1 + vs[i].SrcY = 1 + vs[i].ColorR = float32(c.R) / 0xFF + vs[i].ColorG = float32(c.B) / 0xFF + vs[i].ColorB = float32(c.G) / 0xFF + vs[i].ColorA = 1 + } + + op := &ebiten.DrawTrianglesOptions{} + screen.DrawTriangles(vs, is, whiteSubImage, op) +} diff --git a/game.go b/game.go new file mode 100644 index 0000000..12386d2 --- /dev/null +++ b/game.go @@ -0,0 +1,64 @@ +package main + +import ( + "image/color" + c "loading/character" + "loading/objects" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/text" +) + +// Game implements ebiten.Game interface. +type Game struct { + counter int + player c.Character + enemies []c.Character + keys []ebiten.Key + bobbles []objects.Bobble +} + +// Layout takes the outside size (e.g., the window size) and returns the (logical) screen size. +// If you don't have to adjust the screen size with the outside size, just return a fixed size. +func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { + return 800, 600 +} + +// Draw draws the game screen. +// Draw is called every frame (typically 1/60[s] for 60Hz display). +func (g *Game) Draw(screen *ebiten.Image) { + dst := screen + dst.Fill(color.RGBA{0x28, 0x28, 0x28, 0xff}) + + // render the info text + text.Draw(dst, bsofttext, mplusNormalFont, 20, 40, color.White) + + //g.DrawChase(dst) + //g.DrawLoading(dst) + g.DrawBobbles(dst) +} + +func (g *Game) DrawBobbles(image *ebiten.Image) { + //identifier + text.Draw(image, bobblesInfo, mplusTinyFont, 20, 60, color.White) + + //light up the bobbles + for i := 0; i < len(g.bobbles); i++ { + DrawBobble(g.bobbles[i], image) + } +} + +func (g *Game) DrawChase(image *ebiten.Image) { + + //identifier + text.Draw(image, sceneInfo, mplusTinyFont, 20, 60, color.White) + + //draw the enemies first + for i := 0; i < len(g.enemies); i++ { + DrawCharacter(g.enemies[i], image) + } + + //followed by the player character + DrawCharacter(g.player, image) + +} diff --git a/loader.go b/loader.go new file mode 100644 index 0000000..7f943a7 --- /dev/null +++ b/loader.go @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + "image/color" + c "loading/character" + "loading/objects" + "log" + "math/rand" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" + "github.com/hajimehoshi/ebiten/v2/text" + "golang.org/x/image/font" + "golang.org/x/image/font/opentype" +) + +func init() { + //tt, err := opentype.Parse(fonts.MPlus1pRegular_ttf) + tt, err := opentype.Parse(fonts.PressStart2P_ttf) + if err != nil { + log.Fatal(err) + } + + const dpi = 72 + mplusNormalFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ + Size: 24, + DPI: dpi, + Hinting: font.HintingVertical, + }) + if err != nil { + log.Fatal(err) + } + mplusBigFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ + Size: 48, + DPI: dpi, + Hinting: font.HintingFull, // Use quantization to save glyph cache images. + }) + if err != nil { + log.Fatal(err) + } + mplusTinyFont, err = opentype.NewFace(tt, &opentype.FaceOptions{ + Size: 10, + DPI: dpi, + Hinting: font.HintingVertical, + }) + if err != nil { + log.Fatal(err) + } + + // Adjust the line height. + mplusBigFont = text.FaceWithLineHeight(mplusBigFont, 54) +} + +func loading() { + game := &Game{} + whiteImage.Fill(color.White) + ebiten.SetWindowSize(screenWidth, screenHeight) + ebiten.SetWindowTitle(bsofttext) + fmt.Println(bsofttext) + + //loadCharacters(game) + loadBobbles(game) + + // Call ebiten.RunGame to start your game loop. + if err := ebiten.RunGame(game); err != nil { + log.Fatal(err) + } +} + +func loadBobbles(game *Game) { + + // generate bobbles + gridWidth := bubbleGridSpacing * bubbleGridCols + gridHeight := bubbleGridSpacing * bubbleGridRows + + gridStartX := float32((screenWidth - gridWidth - bubbleGridSpacing) / 2) + gridStartY := float32((screenHeight - gridHeight - bubbleGridSpacing) / 2) + + for i := 0; i < totalBobbles; i++ { + game.bobbles = append(game.bobbles, objects.NewBobble()) + + ix := float32(i%bubbleGridRows*bubbleGridSpacing + i%bubbleGridRows*bubbleGridSpacing) + iy := float32(i/bubbleGridCols*bubbleGridSpacing + i/bubbleGridCols*bubbleGridSpacing) + + game.bobbles[i].Color = color.RGBA{0xFF, 0x00, 0x00, 0xFF} + game.bobbles[i].Bordercolor = color.RGBA{0x80, 0x00, 0x00, 0xFF} + game.bobbles[i].SetPose(gridStartX+ix, gridStartY+iy) + } +} + +func loadCharacters(game *Game) { + // generate player character + game.player = c.NewCharacter() + game.player.SetPose(float32(rand.Intn(screenWidth)), float32(rand.Intn(screenHeight))) + + // generate enemies + for i := 0; i < enemyCount; i++ { + //create new enemy, slip 'em in + game.enemies = append(game.enemies, c.NewCharacter()) + + //set random position based on screen dimensions + game.enemies[i].SetPose(float32(rand.Intn(screenWidth)), float32(rand.Intn(screenHeight))) + + //distinguish them from the player character + game.enemies[i].Color = color.RGBA{0xFF, 0x00, 0x00, 0xFF} + } +} diff --git a/loading.go b/loading.go new file mode 100644 index 0000000..52d3bae --- /dev/null +++ b/loading.go @@ -0,0 +1,23 @@ +package main + +import "loading/scenes" + +type Loading struct { + //Manager scenes.SceneManager +} + +func init() { + +} + +func (l *Loading) Update() error { + return nil +} + +func (l *Loading) Draw() { + +} + +func (l *Loading) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { + return 800, 600 +} diff --git a/main.go b/main.go index c1f74f6..d725295 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,38 @@ package main +import ( + "image" + + "github.com/hajimehoshi/ebiten/v2" + "golang.org/x/image/font" +) + +const ( + screenWidth = 800 + screenHeight = 600 + bsofttext = `bsoft games` + versionInfo = `loading screen MKI` + sceneInfo = `scene a` + bobblesInfo = `bobbles` + enemyCount = 12 + totalBobbles = 9 + bubbleGridRows = 3 + bubbleGridCols = 3 + bubbleGridSpacing = 40 +) + +var ( + mplusNormalFont font.Face + mplusBigFont font.Face + mplusTinyFont font.Face + tileWidth float32 = 20 + tileHeight float32 = 20 + numTiles int = 5 + maxPercent float64 = 100 + whiteImage = ebiten.NewImage(3, 3) + whiteSubImage = whiteImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image) +) + func main() { loading() } diff --git a/objects/bobble.go b/objects/bobble.go new file mode 100644 index 0000000..12e5737 --- /dev/null +++ b/objects/bobble.go @@ -0,0 +1,40 @@ +package objects + +import ( + "image/color" + p "loading/point" + "math/rand" + "strconv" +) + +const ( + defaultRadius = 20 +) + +type Bobble struct { + Radius float32 + Pose p.Point + Id string + Color color.RGBA + Bordercolor color.RGBA +} + +func NewBobble() Bobble { + //default bobble values, with white center and black border + return Bobble{ + Radius: defaultRadius, + Pose: p.Point{X: 0, Y: 0}, + Color: color.RGBA{0xFF, 0xFF, 0xFF, 0xFF}, + Bordercolor: color.RGBA{0x00, 0x00, 0x00, 0xFF}, + Id: "ID-" + strconv.Itoa(rand.Intn(100)), + } +} + +func GetDefaultRadius() float32 { + return float32(defaultRadius) +} + +func (b *Bobble) SetPose(x float32, y float32) { + b.Pose.X = x + b.Pose.Y = y +} diff --git a/scenes/menu.go b/scenes/menu.go new file mode 100644 index 0000000..8efef6d --- /dev/null +++ b/scenes/menu.go @@ -0,0 +1,23 @@ +package scenes + +import "github.com/hajimehoshi/ebiten/v2" + +type Menu struct { +} + +func CreateNewMenu() Menu { + return Menu{} + +} + +func (m *Menu) Draw(screen *ebiten.Image) { + +} + +func (m *Menu) Update() error { + return nil +} + +func (m *Menu) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { + return 800, 600 +} diff --git a/scenes/scene.go b/scenes/scene.go new file mode 100644 index 0000000..7992c23 --- /dev/null +++ b/scenes/scene.go @@ -0,0 +1,9 @@ +package scenes + +import "github.com/hajimehoshi/ebiten/v2" + +type Scene interface { + Update() error + Draw(screen *ebiten.Image) + Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) +} diff --git a/scenes/splash.go b/scenes/splash.go new file mode 100644 index 0000000..a58bd87 --- /dev/null +++ b/scenes/splash.go @@ -0,0 +1 @@ +package scenes diff --git a/updater.go b/updater.go new file mode 100644 index 0000000..f2aac43 --- /dev/null +++ b/updater.go @@ -0,0 +1,126 @@ +package main + +import ( + "loading/objects" + "math" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +// Update proceeds the game state. +// Update is called every tick (1/60 [s] by default). +func (g *Game) Update() error { + // Write your game's logical update. + g.counter++ + + g.UpdatePlayer() + //g.UpdateEnemies() + g.UpdateBobbles() + + return nil +} + +func (g *Game) UpdateBobbles() { + + for i := 0; i < len(g.bobbles); i++ { + bobby := &g.bobbles[i] + bobby.Radius = objects.GetDefaultRadius() + float32(10*math.Sin(float64(g.counter)/(math.Pi*4))) + } + +} + +func (g *Game) UpdatePlayer() { + + //update player angle based on mouse position + mx, my := ebiten.CursorPosition() + playerAngle := float32(math.Atan2(float64(float32(my)-g.player.Pose.Y), float64(float32(mx)-g.player.Pose.X))) + g.player.Angle = playerAngle + + //update position based on wasd input + g.keys = inpututil.AppendPressedKeys(g.keys[:0]) + step := float32(10) + for _, k := range g.keys { + switch k { + case ebiten.KeyW: + g.player.Pose.Y = g.player.Pose.Y - step + case ebiten.KeyA: + g.player.Pose.X = g.player.Pose.X - step + case ebiten.KeyS: + g.player.Pose.Y = g.player.Pose.Y + step + case ebiten.KeyD: + g.player.Pose.X = g.player.Pose.X + step + default: + } + } + +} + +func (g *Game) UpdateEnemies() { + /* + we're going to rotate the enemies so they slowly face the player, to do this we'll find the angle delta + between the player and the enemy via atan2, then add approximately one quarter of this value to the + current enemy angle + */ + /* + for _, e := range g.enemies { + //diff := float32(math.Atan2(float64(g.player.Pose.Y-e.Pose.Y), float64(g.player.Pose.X-e.Pose.X))) + e.Angle = e.Angle + math.Pi/4 + } + */ + + for i := 0; i < len(g.enemies); i++ { + enemy := &g.enemies[i] + + //find distancees between enemy and player + dx := float64(g.player.Pose.X - enemy.Pose.X) + dy := float64(g.player.Pose.Y - enemy.Pose.Y) + + //current sign + currentSign := enemy.DeltaAngle / float32(math.Abs(float64(enemy.DeltaAngle))) + + targetAngle := float32(math.Atan2(dy, dx)) + math.Pi*2 + + //_, absTargetAngle := math.Modf(float64(targetAngle)) + + phi := float32(math.Mod(float64(targetAngle-enemy.Angle), float64(2*math.Pi))) + if phi > math.Pi { + enemy.DeltaAngle = math.Pi - phi + } else { + enemy.DeltaAngle = phi + } + + //enemy.DeltaAngle = float32(math.Min(2*math.Pi-math.Abs(float64(enemy.Angle-targetAngle)), math.Abs(float64(enemy.Angle-targetAngle)))) + + enemy.Angle = enemy.Angle + enemy.DeltaAngle/16 + + enemy.SignFlip = currentSign + + for j := 0; j < len(g.enemies); j++ { + if j == i { + continue + } + + xdist := (g.enemies[j].Pose.X - enemy.Pose.X) + ydist := (g.enemies[j].Pose.Y - enemy.Pose.Y) + + //quick boundary check before we get to the algebra + if xdist > enemy.Buffer || ydist > enemy.Buffer { + continue + } + + dist := float32(math.Sqrt(float64(xdist*xdist + ydist*ydist))) + if dist < enemy.Buffer { + enemy.Pose.X = enemy.Pose.X + dist*float32(math.Cos(float64(targetAngle))) + enemy.Pose.Y = enemy.Pose.Y + dist*float32(math.Sin(float64(targetAngle))) + } + + } + + enemy.Pose.X = enemy.Pose.X + float32(dx)/360 + enemy.Pose.Y = enemy.Pose.Y + float32(dy)/360 + + //enemy.Angle = enemy.Angle + enemy.DeltaAngle/(math.Pi/4) + } + +}