1st commit
12
Makefile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
all: clean build
|
||||||
|
@echo ok
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f openquell
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build
|
||||||
|
|
||||||
|
|
||||||
|
test:
|
||||||
|
@echo 1
|
||||||
9
TODO.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
## Levels:
|
||||||
|
|
||||||
|
- get rid of floor image
|
||||||
|
- use background image over whole screen size
|
||||||
|
- put level on top of it
|
||||||
|
- paint level in levels/**.lvl on whole screen into the middle
|
||||||
|
- use first line as background image name
|
||||||
|
- ignore comments in lvl files
|
||||||
|
- add whitespace-mode flag in lvl files
|
||||||
29
assets/levels/gen.sh
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
width=$(grep "width int" ../main.go | awk '{print $4}')
|
||||||
|
height=$(grep "height int" ../main.go | awk '{print $4}')
|
||||||
|
|
||||||
|
read -p " Enter level name: " name
|
||||||
|
read -p " Enter background: " background
|
||||||
|
read -p "Enter description: " des
|
||||||
|
|
||||||
|
if test -z "$name"; then
|
||||||
|
name="newlevel"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if test -z "$bbackground"; then
|
||||||
|
background="background-lila"
|
||||||
|
fi
|
||||||
|
|
||||||
|
(
|
||||||
|
echo "Description: $des"
|
||||||
|
echo "Background: $background"
|
||||||
|
w=$(($width / 32))
|
||||||
|
h=$(($height / 32))
|
||||||
|
|
||||||
|
for x in $(seq 1 $h); do
|
||||||
|
for y in $(seq 1 $w); do
|
||||||
|
echo -n " "
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
done
|
||||||
|
)
|
||||||
17
assets/levels/gurke.lvl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
Description: find the fruit
|
||||||
|
Background: background-orange
|
||||||
|
|
||||||
|
|
||||||
|
##############
|
||||||
|
# #
|
||||||
|
############ #
|
||||||
|
# S #
|
||||||
|
# ############
|
||||||
|
# #
|
||||||
|
############ #
|
||||||
|
# # # #
|
||||||
|
# ##### # # #
|
||||||
|
# # #
|
||||||
|
##############
|
||||||
|
|
||||||
|
|
||||||
17
assets/levels/intro.lvl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
Description: Introduction: collect the goods by moving the ball onto them
|
||||||
|
Background: background-lila
|
||||||
|
####################
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# ######
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
####################
|
||||||
150
assets/loader-levels.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
_ "image/png"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"openquell/util"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Levels = LoadLevels("levels")
|
||||||
|
var Tiles = InitTiles()
|
||||||
|
|
||||||
|
// Tile: contains image, identifier (as used in level data) and
|
||||||
|
// additional properties
|
||||||
|
type Tile struct {
|
||||||
|
Id byte
|
||||||
|
Sprite *ebiten.Image
|
||||||
|
Class string
|
||||||
|
Solid bool
|
||||||
|
Player bool
|
||||||
|
Renderable bool
|
||||||
|
Velocity bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTilePlayer() *Tile {
|
||||||
|
return &Tile{
|
||||||
|
Id: 'S',
|
||||||
|
Sprite: Assets["sphere-blue"],
|
||||||
|
Class: "sphere",
|
||||||
|
Renderable: true,
|
||||||
|
Player: true,
|
||||||
|
Velocity: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTileBlock(class string) *Tile {
|
||||||
|
return &Tile{
|
||||||
|
Id: '#',
|
||||||
|
Sprite: Assets[class],
|
||||||
|
Class: class,
|
||||||
|
Solid: true,
|
||||||
|
Renderable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// used to map level data bytes to actual tiles
|
||||||
|
type TileRegistry map[byte]*Tile
|
||||||
|
|
||||||
|
// holds a raw level spec:
|
||||||
|
//
|
||||||
|
// Name: the name of the level file w/o the .lvl extension
|
||||||
|
// Background: an image name used as game background for this level
|
||||||
|
// Description: text to display on top
|
||||||
|
// Data: a level spec consisting of chars of the above mapping and spaces, e.g.:
|
||||||
|
// ####
|
||||||
|
// # #
|
||||||
|
// ####
|
||||||
|
//
|
||||||
|
// Each level data must be 20 chars wide (= 640 px width) and 15 chars
|
||||||
|
// high (=480 px height).
|
||||||
|
type RawLevel struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Background *ebiten.Image
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitTiles() TileRegistry {
|
||||||
|
return TileRegistry{
|
||||||
|
' ': {Id: ' ', Class: "floor", Renderable: false},
|
||||||
|
'#': NewTileBlock("block-grey32"),
|
||||||
|
'S': NewTilePlayer(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load levels at compile time into ram, creates a slice of raw levels
|
||||||
|
func LoadLevels(dir string) []RawLevel {
|
||||||
|
levels := []RawLevel{}
|
||||||
|
|
||||||
|
// we use embed.FS to iterate over all files in ./levels/
|
||||||
|
entries, err := assetfs.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read level dir %s: %s", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, levelfile := range entries {
|
||||||
|
if levelfile.Type().IsRegular() && strings.Contains(levelfile.Name(), ".lvl") {
|
||||||
|
path := filepath.Join("assets", dir)
|
||||||
|
fmt.Printf("LOADING level %s/%s ... ", path, levelfile)
|
||||||
|
level := ParseRawLevel(path, levelfile)
|
||||||
|
fmt.Printf("done\n")
|
||||||
|
levels = append(levels, level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return levels
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseRawLevel(dir string, levelfile fs.DirEntry) RawLevel {
|
||||||
|
fd, err := os.Open(filepath.Join(dir, levelfile.Name()))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read level file %s: %s", levelfile.Name(), err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
name := strings.TrimSuffix(levelfile.Name(), ".lvl")
|
||||||
|
des := ""
|
||||||
|
background := &ebiten.Image{}
|
||||||
|
data := []byte{}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(fd)
|
||||||
|
for scanner.Scan() {
|
||||||
|
// ignore any whitespace
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// ignore empty lines
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Contains(line, "Background:"):
|
||||||
|
haveit := strings.Split(line, ": ")
|
||||||
|
if util.Exists(Assets, haveit[1]) {
|
||||||
|
background = Assets[haveit[1]]
|
||||||
|
}
|
||||||
|
case strings.Contains(line, "Description:"):
|
||||||
|
haveit := strings.Split(line, ": ")
|
||||||
|
des = haveit[1]
|
||||||
|
default:
|
||||||
|
// all other non-empty and non-equalsign lines are
|
||||||
|
// level definition matrix data, merge thes into data
|
||||||
|
data = append(data, line+"\n"...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RawLevel{
|
||||||
|
Name: name,
|
||||||
|
Data: data,
|
||||||
|
Background: background,
|
||||||
|
Description: des,
|
||||||
|
}
|
||||||
|
}
|
||||||
58
assets/loader-sprites.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
_ "image/png"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Maps image name to image data
|
||||||
|
type AssetRegistry map[string]*ebiten.Image
|
||||||
|
|
||||||
|
//go:embed levels/*.lvl sprites/*.png
|
||||||
|
var assetfs embed.FS
|
||||||
|
|
||||||
|
var Assets = LoadImages("sprites")
|
||||||
|
|
||||||
|
func LoadImages(dir string) AssetRegistry {
|
||||||
|
images := AssetRegistry{}
|
||||||
|
|
||||||
|
// we use embed.FS to iterate over all files in ./assets/
|
||||||
|
entries, err := assetfs.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to read assets dir %s: %s", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
fmt.Println(entry.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, imagefile := range entries {
|
||||||
|
path := filepath.Join("assets", dir, imagefile.Name())
|
||||||
|
fmt.Printf("LOADING %s ... ", path)
|
||||||
|
fd, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to open image file %s: %s", imagefile.Name(), err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
name := strings.TrimSuffix(imagefile.Name(), ".png")
|
||||||
|
|
||||||
|
img, _, err := image.Decode(fd)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to decode image %s: %s", imagefile.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
images[name] = ebiten.NewImageFromImage(img)
|
||||||
|
fmt.Printf("done\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return images
|
||||||
|
}
|
||||||
BIN
assets/sprites/background-lila.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
assets/sprites/background-orange.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
assets/sprites/block-grey.png
Normal file
|
After Width: | Height: | Size: 584 B |
BIN
assets/sprites/block-grey32.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/sprites/block-orange-32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/sprites/sphere-blue.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
17
components/components.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// virtual location, aka tile address
|
||||||
|
|
||||||
|
type Renderable struct {
|
||||||
|
Image *ebiten.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
// only tile entities will have those
|
||||||
|
type Tilish struct{}
|
||||||
|
type Solid struct{}
|
||||||
|
type Floor struct{}
|
||||||
|
type Player struct{}
|
||||||
106
components/position.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
. "openquell/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// physical location on screen
|
||||||
|
type Position struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Cellsize int
|
||||||
|
Rect *image.Rectangle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (position *Position) Update(x, y int, size ...int) {
|
||||||
|
if len(size) == 1 {
|
||||||
|
position.Cellsize = size[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
position.X = x
|
||||||
|
position.Y = y
|
||||||
|
|
||||||
|
rect := image.Rect(x, y, x+position.Cellsize, y+position.Cellsize)
|
||||||
|
position.Rect = &rect
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPosition(point *image.Point, cellsize int) *Position {
|
||||||
|
position := &Position{}
|
||||||
|
position.Update(point.X*cellsize, point.Y*cellsize, cellsize)
|
||||||
|
return position
|
||||||
|
}
|
||||||
|
|
||||||
|
func (position *Position) GetMoved(velosity *Velocity) *Position {
|
||||||
|
pos := &Position{}
|
||||||
|
pos.Update(position.X+velosity.Data.X, position.Y+velosity.Data.Y, position.Cellsize)
|
||||||
|
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (position *Position) Point() *image.Point {
|
||||||
|
return &image.Point{position.X / position.Cellsize, position.Y / position.Cellsize}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (position *Position) Left() int {
|
||||||
|
return position.X
|
||||||
|
}
|
||||||
|
|
||||||
|
func (position *Position) Right() int {
|
||||||
|
return position.X + position.Cellsize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (position *Position) Top() int {
|
||||||
|
return position.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
func (position *Position) Bottom() int {
|
||||||
|
return position.Y + position.Cellsize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (position *Position) String() string {
|
||||||
|
return fmt.Sprintf("[%d,%d](Left:%d,Top:%d) x (Right:%d,Bottom:%d)",
|
||||||
|
position.X/32,
|
||||||
|
position.Y/32,
|
||||||
|
position.X,
|
||||||
|
position.Y,
|
||||||
|
position.Right(),
|
||||||
|
position.Bottom(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (position *Position) Move(velocity *Velocity) {
|
||||||
|
position.Update(position.X+velocity.Data.X, position.Y+velocity.Data.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (position *Position) Set(newpos *Position) {
|
||||||
|
position.Update(newpos.X, newpos.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tile *Position) Intersects(moving *Position, velocity *Velocity) (bool, *Position) {
|
||||||
|
object := moving.GetMoved(velocity)
|
||||||
|
|
||||||
|
is := tile.Rect.Bounds().Intersect(object.Rect.Bounds())
|
||||||
|
if is != image.ZR {
|
||||||
|
fmt.Printf(" velocity: %d,%d\n", velocity.Data.X, velocity.Data.Y)
|
||||||
|
fmt.Printf(" player: %s\n", moving.Rect)
|
||||||
|
fmt.Printf("moved player: %s\n", object.Rect)
|
||||||
|
fmt.Printf("collision at: %s\n", is)
|
||||||
|
// collision, snap into neighbouring tile depending on the direction
|
||||||
|
switch velocity.Direction {
|
||||||
|
case West:
|
||||||
|
object.Update(tile.Rect.Max.X, tile.Y)
|
||||||
|
case East:
|
||||||
|
object.Update(tile.Rect.Min.X-tile.Cellsize, tile.Y)
|
||||||
|
case South:
|
||||||
|
object.Update(tile.X, tile.Rect.Min.Y-tile.Cellsize)
|
||||||
|
case North:
|
||||||
|
object.Update(tile.X, tile.Rect.Max.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, object
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
39
components/velocity.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package components
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "openquell/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// movement in relation to position
|
||||||
|
type Velocity struct {
|
||||||
|
Data Position
|
||||||
|
Direction int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (velocity *Velocity) Change(direction int) {
|
||||||
|
ticks := 4
|
||||||
|
|
||||||
|
switch direction {
|
||||||
|
case East:
|
||||||
|
velocity.Data.X = ticks
|
||||||
|
velocity.Data.Y = 0
|
||||||
|
case West:
|
||||||
|
velocity.Data.X = ticks - (ticks * 2)
|
||||||
|
velocity.Data.Y = 0
|
||||||
|
case South:
|
||||||
|
velocity.Data.X = 0
|
||||||
|
velocity.Data.Y = ticks
|
||||||
|
case North:
|
||||||
|
velocity.Data.X = 0
|
||||||
|
velocity.Data.Y = ticks - (ticks * 2)
|
||||||
|
case Stop:
|
||||||
|
velocity.Data.X = 0
|
||||||
|
velocity.Data.Y = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
velocity.Direction = direction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (velocity *Velocity) Moving() bool {
|
||||||
|
return velocity.Data.X != 0 || velocity.Data.Y != 0
|
||||||
|
}
|
||||||
9
config/static.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
const (
|
||||||
|
Stop = iota
|
||||||
|
East
|
||||||
|
West
|
||||||
|
South
|
||||||
|
North
|
||||||
|
)
|
||||||
51
game/game.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/mlange-42/arche/ecs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
World *ecs.World
|
||||||
|
Bounds image.Rectangle
|
||||||
|
ScreenWidth, ScreenHeight int
|
||||||
|
CurrentLevel int
|
||||||
|
Scenes []Scene
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame(width, height, startlevel int) *Game {
|
||||||
|
world := ecs.NewWorld()
|
||||||
|
|
||||||
|
game := &Game{
|
||||||
|
Bounds: image.Rectangle{},
|
||||||
|
CurrentLevel: startlevel,
|
||||||
|
World: &world,
|
||||||
|
ScreenWidth: width,
|
||||||
|
ScreenHeight: height,
|
||||||
|
}
|
||||||
|
|
||||||
|
levelscene := NewLevelScene(game, startlevel)
|
||||||
|
game.Scenes = append(game.Scenes, levelscene)
|
||||||
|
|
||||||
|
return game
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game *Game) Update() error {
|
||||||
|
for _, scene := range game.Scenes {
|
||||||
|
scene.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (game *Game) Draw(screen *ebiten.Image) {
|
||||||
|
for _, scene := range game.Scenes {
|
||||||
|
scene.Draw(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) Layout(newWidth, newHeight int) (int, int) {
|
||||||
|
return g.ScreenWidth, g.ScreenHeight
|
||||||
|
}
|
||||||
130
game/grid.go
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"log"
|
||||||
|
"openquell/assets"
|
||||||
|
"openquell/components"
|
||||||
|
. "openquell/config"
|
||||||
|
|
||||||
|
"github.com/mlange-42/arche/generic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Grid struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Size int
|
||||||
|
Tilesize int
|
||||||
|
TilesX int
|
||||||
|
TilesY int
|
||||||
|
Map map[image.Point]*assets.Tile
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: put component addition into extra callable function, to be called
|
||||||
|
// by game.go in a level-loop. Also remove components of previous level
|
||||||
|
func NewGrid(game *Game, tilesize int, mapslice map[image.Point]*assets.Tile) *Grid {
|
||||||
|
// we use this to turn our tiles into iterable entities, used for
|
||||||
|
// collision detection, transformation and other things
|
||||||
|
playermapper := generic.NewMap4[
|
||||||
|
components.Position,
|
||||||
|
components.Velocity,
|
||||||
|
components.Renderable,
|
||||||
|
components.Player](game.World)
|
||||||
|
solidmapper := generic.NewMap4[
|
||||||
|
components.Position,
|
||||||
|
components.Renderable,
|
||||||
|
components.Tilish,
|
||||||
|
components.Solid](game.World)
|
||||||
|
emptymapper := generic.NewMap2[components.Position, components.Tilish](game.World)
|
||||||
|
|
||||||
|
var pos *components.Position
|
||||||
|
var render *components.Renderable
|
||||||
|
|
||||||
|
for point, tile := range mapslice {
|
||||||
|
switch tile.Renderable {
|
||||||
|
case true:
|
||||||
|
switch {
|
||||||
|
case tile.Solid:
|
||||||
|
entity := solidmapper.New()
|
||||||
|
pos, render, _, _ = solidmapper.Get(entity)
|
||||||
|
case tile.Player:
|
||||||
|
entity := playermapper.New()
|
||||||
|
pos, _, render, _ = playermapper.Get(entity)
|
||||||
|
fmt.Printf("player start pos: %d,%d\n", point.X*tilesize, point.Y*tilesize)
|
||||||
|
default:
|
||||||
|
log.Fatalln("unsupported tile type encountered")
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Image = tile.Sprite
|
||||||
|
|
||||||
|
default:
|
||||||
|
// empty cell, this is where the player[s] move. No
|
||||||
|
// sprite required since every level has a background
|
||||||
|
// image.
|
||||||
|
entity := emptymapper.New()
|
||||||
|
pos, _ = emptymapper.Get(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// however, every tile has a position
|
||||||
|
pos.Update(point.X*tilesize, point.Y*tilesize, tilesize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Grid{
|
||||||
|
Size: len(mapslice),
|
||||||
|
Tilesize: tilesize,
|
||||||
|
Width: game.ScreenWidth,
|
||||||
|
Height: game.ScreenHeight,
|
||||||
|
TilesX: game.ScreenWidth / tilesize,
|
||||||
|
TilesY: game.ScreenHeight / tilesize,
|
||||||
|
Map: mapslice,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (grid *Grid) GetSolidNeighborPosition(
|
||||||
|
position *components.Position,
|
||||||
|
velocity *components.Velocity,
|
||||||
|
solid bool) (bool, *components.Position) {
|
||||||
|
|
||||||
|
if !solid {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(position)
|
||||||
|
// set to true, ifwe are already on the last tile in the current
|
||||||
|
// direction, i.e. on the edge of the grid
|
||||||
|
edge := true
|
||||||
|
neighborpos := position.Point()
|
||||||
|
|
||||||
|
switch velocity.Direction {
|
||||||
|
case East:
|
||||||
|
if neighborpos.X < grid.TilesX {
|
||||||
|
neighborpos.X++
|
||||||
|
edge = false
|
||||||
|
}
|
||||||
|
case West:
|
||||||
|
if neighborpos.X > 0 {
|
||||||
|
neighborpos.X--
|
||||||
|
edge = false
|
||||||
|
}
|
||||||
|
case South:
|
||||||
|
if neighborpos.Y < grid.TilesY {
|
||||||
|
neighborpos.Y++
|
||||||
|
edge = false
|
||||||
|
}
|
||||||
|
case North:
|
||||||
|
if neighborpos.Y > 0 {
|
||||||
|
neighborpos.Y--
|
||||||
|
edge = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newpos := components.NewPosition(neighborpos, grid.Tilesize)
|
||||||
|
fmt.Println(newpos, edge)
|
||||||
|
|
||||||
|
if !edge && grid.Map[*neighborpos].Solid {
|
||||||
|
return true, newpos
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
172
game/levels.go
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"log"
|
||||||
|
"openquell/assets"
|
||||||
|
"openquell/components"
|
||||||
|
. "openquell/config"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/mlange-42/arche/ecs"
|
||||||
|
"github.com/mlange-42/arche/filter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Level struct {
|
||||||
|
Grid *Grid
|
||||||
|
Cellsize, Width, Height int
|
||||||
|
World *ecs.World
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Background *ebiten.Image
|
||||||
|
Mapslice map[image.Point]*assets.Tile
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level {
|
||||||
|
//grid := NewGrid(game, cellsize, plan)
|
||||||
|
|
||||||
|
return &Level{
|
||||||
|
//Grid: grid,
|
||||||
|
Mapslice: LevelToSlice(game, plan, cellsize),
|
||||||
|
Cellsize: cellsize,
|
||||||
|
World: game.World,
|
||||||
|
Width: game.ScreenWidth,
|
||||||
|
Height: game.ScreenHeight,
|
||||||
|
Description: plan.Description,
|
||||||
|
Background: plan.Background,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (level *Level) Update() {
|
||||||
|
//playermapper := generic.NewMap3[components.Position, components.Velocity, components.Renderable](level.World)
|
||||||
|
positionid := ecs.ComponentID[components.Position](level.World)
|
||||||
|
velocityid := ecs.ComponentID[components.Velocity](level.World)
|
||||||
|
playerid := ecs.ComponentID[components.Player](level.World)
|
||||||
|
//solidid := ecs.ComponentID[components.Solid](level.World)
|
||||||
|
//renderid := ecs.ComponentID[components.Renderable](level.World)
|
||||||
|
|
||||||
|
selector := filter.All(positionid, velocityid, playerid)
|
||||||
|
query := level.World.Query(selector)
|
||||||
|
|
||||||
|
//tileselector := filter.All(positionid, solidid, renderid)
|
||||||
|
//tilequery := level.World.Query(tileselector)
|
||||||
|
|
||||||
|
for query.Next() {
|
||||||
|
playerposition := (*components.Position)(query.Get(positionid))
|
||||||
|
velocity := (*components.Velocity)(query.Get(velocityid))
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ebiten.IsKeyPressed(ebiten.KeyRight):
|
||||||
|
velocity.Change(East)
|
||||||
|
case ebiten.IsKeyPressed(ebiten.KeyLeft):
|
||||||
|
velocity.Change(West)
|
||||||
|
case ebiten.IsKeyPressed(ebiten.KeyDown):
|
||||||
|
velocity.Change(South)
|
||||||
|
case ebiten.IsKeyPressed(ebiten.KeyUp):
|
||||||
|
velocity.Change(North)
|
||||||
|
// other keys: <tab>: switch player, etc
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Use this to check against obstacles, collectibles etc
|
||||||
|
for tilequery.Next() {
|
||||||
|
tilepos := (*components.Position)(tilequery.Get(positionid))
|
||||||
|
|
||||||
|
if velocity.Moving() {
|
||||||
|
intersects, newpos := tilepos.Intersects(playerposition, velocity)
|
||||||
|
if intersects {
|
||||||
|
fmt.Printf("collision detected. tile: %s\n", tilepos)
|
||||||
|
fmt.Printf(" player: %s\n", playerposition)
|
||||||
|
fmt.Printf(" new: %s\n", newpos)
|
||||||
|
|
||||||
|
playerposition.Set(newpos)
|
||||||
|
fmt.Printf(" player new: %s\n", playerposition)
|
||||||
|
velocity.Change(Stop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if velocity.Moving() {
|
||||||
|
ok, tilepos := level.Grid.GetSolidNeighborPosition(playerposition, velocity, true)
|
||||||
|
if ok {
|
||||||
|
intersects, newpos := tilepos.Intersects(playerposition, velocity)
|
||||||
|
if intersects {
|
||||||
|
fmt.Printf("collision detected. tile: %s\n", tilepos)
|
||||||
|
fmt.Printf(" player: %s\n", playerposition)
|
||||||
|
fmt.Printf(" new: %s\n", newpos)
|
||||||
|
|
||||||
|
playerposition.Set(newpos)
|
||||||
|
fmt.Printf(" player new: %s\n", playerposition)
|
||||||
|
velocity.Change(Stop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerposition.Move(velocity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (level *Level) Position2Point(position *components.Position) image.Point {
|
||||||
|
return image.Point{
|
||||||
|
int(position.X) / level.Cellsize,
|
||||||
|
int(position.Y) / level.Cellsize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the tile the moving object would end up on if indeed moving
|
||||||
|
func (level *Level) GetTile(position *components.Position, velocity *components.Velocity) *assets.Tile {
|
||||||
|
newpoint := image.Point{
|
||||||
|
int(position.X+velocity.Data.X) / level.Cellsize,
|
||||||
|
int(position.Y+velocity.Data.Y) / level.Cellsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
tile := level.Mapslice[newpoint]
|
||||||
|
return tile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (level *Level) Draw(screen *ebiten.Image) {
|
||||||
|
// FIXME: move out
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
screen.DrawImage(level.Background, op)
|
||||||
|
|
||||||
|
rid := ecs.ComponentID[components.Renderable](level.World)
|
||||||
|
pid := ecs.ComponentID[components.Position](level.World)
|
||||||
|
|
||||||
|
selector := filter.All(rid, pid)
|
||||||
|
|
||||||
|
query := level.World.Query(selector)
|
||||||
|
for query.Next() {
|
||||||
|
pos := (*components.Position)(query.Get(pid))
|
||||||
|
sprite := (*components.Renderable)(query.Get(rid))
|
||||||
|
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Translate(float64(pos.X), float64(pos.Y))
|
||||||
|
|
||||||
|
screen.DrawImage(sprite.Image, op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (level *Level) SetupGrid(game *Game) {
|
||||||
|
level.Grid = NewGrid(game, level.Cellsize, level.Mapslice)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LevelToSlice(game *Game, level *assets.RawLevel, tilesize int) map[image.Point]*assets.Tile {
|
||||||
|
size := game.ScreenWidth * game.ScreenHeight
|
||||||
|
mapslice := make(map[image.Point]*assets.Tile, size)
|
||||||
|
|
||||||
|
for y, line := range strings.Split(string(level.Data), "\n") {
|
||||||
|
if len(line) != game.ScreenWidth/tilesize && y < game.ScreenHeight/tilesize {
|
||||||
|
log.Fatalf("line %d doesn't contain %d tiles, but %d",
|
||||||
|
y, game.ScreenWidth/tilesize, len(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
for x, char := range line {
|
||||||
|
mapslice[image.Point{x, y}] = assets.Tiles[byte(char)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapslice
|
||||||
|
}
|
||||||
51
game/scene.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"openquell/assets"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wrapper for different screens to be shown, as Welcome, Options,
|
||||||
|
// About, Select Level and of course the actual Levels.
|
||||||
|
// Scenes are responsible for screen clearing! That way a scene is able
|
||||||
|
// to render its content onto the running level, e.g. the options scene
|
||||||
|
// etc.
|
||||||
|
type Scene interface {
|
||||||
|
Update() error
|
||||||
|
Draw(screen *ebiten.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LevelScene struct {
|
||||||
|
Game *Game
|
||||||
|
CurrentLevel int
|
||||||
|
Levels []*Level
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements the actual playing Scene
|
||||||
|
func NewLevelScene(game *Game, startlevel int) Scene {
|
||||||
|
scene := &LevelScene{Game: game, CurrentLevel: startlevel}
|
||||||
|
|
||||||
|
scene.GenerateLevels()
|
||||||
|
scene.Levels[game.CurrentLevel].SetupGrid(game)
|
||||||
|
fmt.Println(game.World.Stats().String())
|
||||||
|
|
||||||
|
return scene
|
||||||
|
}
|
||||||
|
|
||||||
|
func (scene *LevelScene) GenerateLevels() {
|
||||||
|
for _, level := range assets.Levels {
|
||||||
|
scene.Levels = append(scene.Levels, NewLevel(scene.Game, 32, &level))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (scene *LevelScene) Update() error {
|
||||||
|
scene.Levels[scene.CurrentLevel].Update()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (scene *LevelScene) Draw(screen *ebiten.Image) {
|
||||||
|
screen.Clear()
|
||||||
|
scene.Levels[scene.CurrentLevel].Draw(screen)
|
||||||
|
}
|
||||||
19
go.mod
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
module openquell
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.6.5
|
||||||
|
github.com/mlange-42/arche v0.10.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alecthomas/repr v0.3.0 // indirect
|
||||||
|
github.com/ebitengine/purego v0.5.0 // indirect
|
||||||
|
github.com/jezek/xgb v1.1.0 // indirect
|
||||||
|
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 // indirect
|
||||||
|
golang.org/x/image v0.12.0 // indirect
|
||||||
|
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 // indirect
|
||||||
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
|
golang.org/x/sys v0.12.0 // indirect
|
||||||
|
)
|
||||||
51
go.sum
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8=
|
||||||
|
github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
|
github.com/ebitengine/purego v0.5.0 h1:JrMGKfRIAM4/QVKaesIIT7m/UVjTj5GYhRSQYwfVdpo=
|
||||||
|
github.com/ebitengine/purego v0.5.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.6.5 h1:lALv+qhEK3CBWViyiGpz4YcR6slVJEjCiS7sExKZ9OE=
|
||||||
|
github.com/hajimehoshi/ebiten/v2 v2.6.5/go.mod h1:TZtorL713an00UW4LyvMeKD8uXWnuIuCPtlH11b0pgI=
|
||||||
|
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
|
||||||
|
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||||
|
github.com/mlange-42/arche v0.10.0 h1:fEFDAYMAnWa+xHc1oq4gVcA4PuEQOCGSRXSKITXawMw=
|
||||||
|
github.com/mlange-42/arche v0.10.0/go.mod h1:gJ5J8vBreqrf4TcBomBFPGnWdE5P3qa4LtxYHn1gDcg=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 h1:3AGKexOYqL+ztdWdkB1bDwXgPBuTS/S8A4WzuTvJ8Cg=
|
||||||
|
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
|
||||||
|
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
|
||||||
|
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
|
||||||
|
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 h1:Q6NT8ckDYNcwmi/bmxe+XbiDMXqMRW1xFBtJ+bIpie4=
|
||||||
|
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57/go.mod h1:wEyOn6VvNW7tcf+bW/wBz1sehi2s2BZ4TimyR7qZen4=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||||
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
26
main.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"openquell/game"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
width int = 640
|
||||||
|
height int = 480
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ebiten.SetWindowSize(width, height)
|
||||||
|
ebiten.SetWindowTitle("openquell")
|
||||||
|
|
||||||
|
g := game.NewGame(width, height, 0)
|
||||||
|
|
||||||
|
err := ebiten.RunGame(g)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("unable to run game: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
BIN
src/00111971.jpg
Normal file
|
After Width: | Height: | Size: 371 KiB |
BIN
src/221000622904_001.webp
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
src/block-grey.xcf
Normal file
BIN
src/block-orange-32.xcf
Normal file
BIN
src/floor.png
Normal file
|
After Width: | Height: | Size: 559 B |
BIN
src/floor.xcf
Normal file
BIN
src/kugel.xcf
Normal file
21
util/generics.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
// find an item in a list, generic variant
|
||||||
|
func Contains[E comparable](s []E, v E) bool {
|
||||||
|
for _, vs := range s {
|
||||||
|
if v == vs {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// look if a key in a map exists, generic variant
|
||||||
|
func Exists[K comparable, V any](m map[K]V, v K) bool {
|
||||||
|
if _, ok := m[v]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||