Private
Public Access
1
0

Introduce core components for "Escape From Teacher" game: server, client, physics, asset system, and protocol definitions. Add Docker-Compose setup for Redis and NATS infrastructure.

This commit is contained in:
Sebastian Unterschütz
2026-01-01 15:21:18 +01:00
commit 3099ac42c0
9 changed files with 1384 additions and 0 deletions

718
cmd/builder/main.go Normal file
View File

@@ -0,0 +1,718 @@
package main
import (
"encoding/json"
"fmt"
"image"
"image/color"
_ "image/jpeg"
"image/png"
"io/ioutil"
"log"
"math"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/text"
"github.com/hajimehoshi/ebiten/v2/vector"
"golang.org/x/image/font/basicfont"
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game"
)
// --- CONFIG ---
const (
RawDir = "./assets_raw"
OutFile = "./cmd/client/assets/assets.json"
WidthList = 280 // Etwas breiter für die Bilder
WidthInspect = 300
CanvasWidth = 1280
CanvasHeight = 720
LineHeight = 35 // Höher für Thumbnails
HeaderHeight = 30
TextOffset = 22 // Text vertikal zentrieren
)
// Farben
var (
ColBg = color.RGBA{30, 30, 30, 255}
ColPanel = color.RGBA{40, 44, 52, 255}
ColText = color.RGBA{220, 220, 220, 255}
ColHighlight = color.RGBA{80, 120, 200, 255}
ColNewFile = color.RGBA{150, 150, 150, 255}
ColAxis = color.RGBA{100, 255, 100, 255}
ColInput = color.RGBA{20, 20, 20, 255}
ColDelete = color.RGBA{255, 100, 100, 255}
ColPlayerRef = color.RGBA{0, 255, 255, 100}
)
var AssetTypes = []string{"obstacle", "platform", "powerup", "enemy", "deco", "coin"}
// --- HILFSFUNKTIONEN ---
func generateBrickTexture(w, h int) *ebiten.Image {
img := ebiten.NewImage(w, h)
img.Fill(color.RGBA{80, 80, 90, 255})
brickColor := color.RGBA{160, 80, 40, 255}
brickHighlight := color.RGBA{180, 100, 50, 255}
rows := 2
cols := 4
brickH := float32(h) / float32(rows)
brickW := float32(w) / float32(cols)
padding := float32(2)
for row := 0; row < rows; row++ {
for col := 0; col < cols; col++ {
xOffset := float32(0)
if row%2 != 0 {
xOffset = brickW / 2
}
x := float32(col)*brickW + xOffset
y := float32(row) * brickH
drawBrick := func(bx, by float32) {
vector.DrawFilledRect(img, bx+padding, by+padding, brickW-padding*2, brickH-padding*2, brickColor, false)
vector.DrawFilledRect(img, bx+padding, by+padding, brickW-padding*2, 2, brickHighlight, false)
}
drawBrick(x, y)
if x+brickW > float32(w) {
drawBrick(x-float32(w), y)
}
}
}
return img
}
func saveImageToDisk(img *ebiten.Image, filename string) error {
stdImg := img.SubImage(img.Bounds())
assetDir := filepath.Dir(OutFile)
fullPath := filepath.Join(assetDir, filename)
f, err := os.Create(fullPath)
if err != nil {
return err
}
defer f.Close()
return png.Encode(f, stdImg)
}
// --- EDITOR STRUCT ---
type Editor struct {
manifest game.AssetManifest
assetsImages map[string]*ebiten.Image
rawFiles []string
sortedIDs []string
selectedID string
inputBuffer string
activeField string
listScroll float64
showPlayerRef bool
isDraggingImage bool
isDraggingHitbox bool
dragStart game.Vec2
}
func NewEditor() *Editor {
e := &Editor{
assetsImages: make(map[string]*ebiten.Image),
manifest: game.AssetManifest{Assets: make(map[string]game.AssetDefinition)},
showPlayerRef: true,
}
e.ScanRawFiles()
e.LoadManifest()
e.RebuildList()
if len(e.sortedIDs) > 0 {
e.selectedID = e.sortedIDs[0]
}
return e
}
func (e *Editor) ScanRawFiles() {
e.rawFiles = []string{}
entries, _ := os.ReadDir(RawDir)
for _, f := range entries {
if f.IsDir() {
continue
}
ext := strings.ToLower(filepath.Ext(f.Name()))
if ext != ".png" && ext != ".jpg" {
continue
}
e.rawFiles = append(e.rawFiles, f.Name())
id := strings.TrimSuffix(f.Name(), ext)
path := filepath.Join(RawDir, f.Name())
img, _, err := ebitenutil.NewImageFromFile(path)
if err == nil {
e.assetsImages[id] = img
}
}
sort.Strings(e.rawFiles)
}
func (e *Editor) LoadManifest() {
data, err := ioutil.ReadFile(OutFile)
if err == nil {
var loaded game.AssetManifest
json.Unmarshal(data, &loaded)
for k, v := range loaded.Assets {
e.manifest.Assets[k] = v
// Laden der Bilder für existierende Assets
if v.Filename != "" {
assetPath := filepath.Join(filepath.Dir(OutFile), v.Filename)
if img, _, err := ebitenutil.NewImageFromFile(assetPath); err == nil {
e.assetsImages[v.ID] = img
}
}
}
}
}
func (e *Editor) RebuildList() {
e.sortedIDs = []string{}
for k := range e.manifest.Assets {
e.sortedIDs = append(e.sortedIDs, k)
}
sort.Strings(e.sortedIDs)
}
func (e *Editor) Save() {
data, _ := json.MarshalIndent(e.manifest, "", " ")
_ = ioutil.WriteFile(OutFile, data, 0644)
log.Println("Gespeichert.")
}
func (e *Editor) DeleteAsset(id string) {
delete(e.manifest.Assets, id)
e.RebuildList()
if e.selectedID == id {
if len(e.sortedIDs) > 0 {
e.selectedID = e.sortedIDs[0]
} else {
e.selectedID = ""
}
}
log.Printf("Asset gelöscht: %s", id)
}
func (e *Editor) CreateAssetFromFile(filename string) {
ext := filepath.Ext(filename)
id := strings.TrimSuffix(filename, ext)
// Prüfen ob es schon existiert
if _, ok := e.manifest.Assets[id]; ok {
// Neuen Namen generieren um Überschreiben zu vermeiden
id = fmt.Sprintf("%s_%d", id, time.Now().Unix())
}
img := e.assetsImages[strings.TrimSuffix(filename, ext)] // Versuch Raw Image zu finden
// Falls nicht gefunden (weil neu reinkopiert), versuchen wir es zu laden
if img == nil {
path := filepath.Join(RawDir, filename)
img, _, _ = ebitenutil.NewImageFromFile(path)
}
w, h := 64, 64
if img != nil {
w, h = img.Bounds().Dx(), img.Bounds().Dy()
// Speichern wir das Bild auch in e.assetsImages für die Vorschau
e.assetsImages[id] = img
}
// Wichtig: Wir kopieren das Raw File in den Client Assets Ordner!
// (Simuliert durch Laden und Speichern, oder wir setzen nur den Pfad wenn wir faul sind.
// Sauberer: Wir gehen davon aus, dass der User es händisch kopiert hat ODER wir kopieren es hier.)
// Für diesen Editor nehmen wir an, der User nutzt "RawDir" als Quelle und wir müssten es eigentlich kopieren.
// Simpler Hack: Wir nutzen das File aus RawDir und speichern es als neues PNG im Zielordner.
if img != nil {
saveImageToDisk(img, filename)
}
e.manifest.Assets[id] = game.AssetDefinition{
ID: id, Type: "obstacle", Filename: filename, Scale: 1.0,
Color: game.HexColor{R: 255, G: 0, B: 255, A: 255},
DrawOffX: float64(-w) / 2,
DrawOffY: float64(-h),
Hitbox: game.Rect{W: float64(w), H: float64(h), OffsetX: float64(-w) / 2, OffsetY: float64(-h)},
}
e.RebuildList()
e.selectedID = id
}
func (e *Editor) CreatePlatform() {
w, h := 128, 32
texImg := generateBrickTexture(w, h)
timestamp := time.Now().Unix()
filename := fmt.Sprintf("gen_plat_%d.png", timestamp)
id := fmt.Sprintf("platform_%d", timestamp)
if err := saveImageToDisk(texImg, filename); err != nil {
log.Printf("Fehler beim Speichern: %v", err)
return
}
e.assetsImages[id] = texImg
e.manifest.Assets[id] = game.AssetDefinition{
ID: id,
Type: "platform",
Filename: filename,
Scale: 1.0,
Color: game.HexColor{R: 255, G: 255, B: 255, A: 255},
DrawOffX: float64(-w) / 2,
DrawOffY: float64(-h) / 2,
Hitbox: game.Rect{W: float64(w), H: float64(h), OffsetX: float64(-w) / 2, OffsetY: float64(-h) / 2},
}
e.RebuildList()
e.selectedID = id
}
func (e *Editor) Update() error {
if inpututil.IsKeyJustPressed(ebiten.KeyS) && e.activeField == "" {
e.Save()
}
if inpututil.IsKeyJustPressed(ebiten.KeyP) && e.activeField == "" {
e.showPlayerRef = !e.showPlayerRef
}
mx, my := ebiten.CursorPosition()
// --- LISTE (LINKS) ---
if mx < WidthList {
_, wy := ebiten.Wheel()
e.listScroll -= wy * 20
if e.listScroll < 0 {
e.listScroll = 0
}
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) || inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) {
currentY := 40.0 - e.listScroll
// ASSETS
currentY += float64(LineHeight)
for _, id := range e.sortedIDs {
if float64(my) >= currentY && float64(my) < currentY+float64(LineHeight) {
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
e.selectedID = id
e.activeField = ""
} else {
e.DeleteAsset(id)
return nil
}
}
currentY += float64(LineHeight)
}
// RAW FILES
currentY += 40
for _, f := range e.rawFiles {
ext := filepath.Ext(f)
id := strings.TrimSuffix(f, ext)
if _, exists := e.manifest.Assets[id]; exists {
continue
}
if float64(my) >= currentY && float64(my) < currentY+float64(LineHeight) {
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
e.CreateAssetFromFile(f)
}
}
currentY += float64(LineHeight)
}
if my > CanvasHeight-40 {
e.CreatePlatform()
}
}
return nil
}
// --- INSPECTOR (RECHTS) ---
if mx > CanvasWidth-WidthInspect {
e.UpdateInspector(mx, my)
return nil
}
// --- CANVAS (MITTE) ---
if e.selectedID != "" {
def := e.manifest.Assets[e.selectedID]
centerX := float64(WidthList) + float64(CanvasWidth-WidthList-WidthInspect)/2
centerY := float64(CanvasHeight) * 0.75
relMX := float64(mx) - centerX - def.DrawOffX
relMY := float64(my) - centerY - def.DrawOffY
// Hitbox Drag
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight) {
e.isDraggingHitbox = true
e.dragStart = game.Vec2{X: relMX, Y: relMY}
def.Hitbox.OffsetX = relMX
def.Hitbox.OffsetY = relMY
def.Hitbox.W = 0
def.Hitbox.H = 0
}
if e.isDraggingHitbox {
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
def.Hitbox.W = relMX - def.Hitbox.OffsetX
def.Hitbox.H = relMY - def.Hitbox.OffsetY
} else {
e.isDraggingHitbox = false
// Negative Größen korrigieren
if def.Hitbox.W < 0 {
def.Hitbox.OffsetX += def.Hitbox.W
def.Hitbox.W = math.Abs(def.Hitbox.W)
}
if def.Hitbox.H < 0 {
def.Hitbox.OffsetY += def.Hitbox.H
def.Hitbox.H = math.Abs(def.Hitbox.H)
}
}
e.manifest.Assets[e.selectedID] = def
return nil
}
// Image Drag
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
e.isDraggingImage = true
e.dragStart = game.Vec2{X: float64(mx), Y: float64(my)}
e.activeField = ""
}
if e.isDraggingImage && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
dx := float64(mx) - e.dragStart.X
dy := float64(my) - e.dragStart.Y
def.DrawOffX += dx
def.DrawOffY += dy
e.dragStart = game.Vec2{X: float64(mx), Y: float64(my)}
e.manifest.Assets[e.selectedID] = def
} else {
e.isDraggingImage = false
}
}
return nil
}
func (e *Editor) UpdateInspector(mx, my int) {
if e.selectedID == "" {
return
}
def := e.manifest.Assets[e.selectedID]
y := 40
y += HeaderHeight + 20
// Type toggle
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) && my >= y && my < y+20 {
idx := 0
for i, t := range AssetTypes {
if t == def.Type {
idx = i
}
}
def.Type = AssetTypes[(idx+1)%len(AssetTypes)]
}
y += HeaderHeight
updateFloat := func(fieldID string, val *float64) {
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) && my >= y && my < y+20 {
e.activeField = fieldID
e.inputBuffer = fmt.Sprintf("%.2f", *val)
ebiten.InputChars()
}
if e.activeField == fieldID {
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
if v, err := strconv.ParseFloat(e.inputBuffer, 64); err == nil {
*val = v
}
e.activeField = ""
} else if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
if len(e.inputBuffer) > 0 {
e.inputBuffer = e.inputBuffer[:len(e.inputBuffer)-1]
}
} else {
e.inputBuffer += string(ebiten.InputChars())
}
}
y += LineHeight
}
if def.Filename == "" {
y += 20
updateFloat("proc_w", &def.ProcWidth)
updateFloat("proc_h", &def.ProcHeight)
def.Hitbox.W = def.ProcWidth
def.Hitbox.H = def.ProcHeight
} else {
y += 20
updateFloat("scale", &def.Scale)
}
y += 20
updateFloat("off_x", &def.DrawOffX)
updateFloat("off_y", &def.DrawOffY)
y += 20
updateFloat("hb_x", &def.Hitbox.OffsetX)
updateFloat("hb_y", &def.Hitbox.OffsetY)
updateFloat("hb_w", &def.Hitbox.W)
updateFloat("hb_h", &def.Hitbox.H)
y += 20
updateColorSlider := func(val *uint8) {
sliderRect := image.Rect(CanvasWidth-WidthInspect+10, y, CanvasWidth-10, y+15)
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) && my >= y && my < y+15 {
relX := float64(mx - sliderRect.Min.X)
pct := relX / float64(sliderRect.Dx())
if pct < 0 {
pct = 0
}
if pct > 1 {
pct = 1
}
*val = uint8(pct * 255)
}
y += 20
}
updateColorSlider(&def.Color.R)
updateColorSlider(&def.Color.G)
updateColorSlider(&def.Color.B)
updateColorSlider(&def.Color.A)
e.manifest.Assets[e.selectedID] = def
}
func (e *Editor) Draw(screen *ebiten.Image) {
// --- 1. LISTE LINKS ---
vector.DrawFilledRect(screen, 0, 0, WidthList, CanvasHeight, ColPanel, false)
// Button Neu
btnRect := image.Rect(10, CanvasHeight-35, WidthList-10, CanvasHeight-10)
vector.DrawFilledRect(screen, float32(btnRect.Min.X), float32(btnRect.Min.Y), float32(btnRect.Dx()), float32(btnRect.Dy()), ColHighlight, false)
text.Draw(screen, "+ NEW PLATFORM", basicfont.Face7x13, 20, CanvasHeight-18, color.RGBA{255, 255, 255, 255})
// SCROLL BEREICH
startY := 40.0 - e.listScroll
currentY := startY
// Helper Funktion zum Zeichnen von Listeneinträgen mit Bild
drawListItem := func(label string, id string, col color.Color, img *ebiten.Image) {
if currentY > -float64(LineHeight) && currentY < CanvasHeight-50 {
// Bild Vorschau (Thumbnail)
if img != nil {
// Skalierung berechnen (max 28px hoch/breit)
iconSize := float64(LineHeight - 4) // etwas Rand lassen
iw, ih := img.Bounds().Dx(), img.Bounds().Dy()
scale := iconSize / float64(ih)
if float64(iw)*scale > iconSize {
scale = iconSize / float64(iw)
}
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(scale, scale)
// Zentrieren im Icon-Bereich (links)
op.GeoM.Translate(10, currentY+2)
screen.DrawImage(img, op)
} else {
// Platzhalter Box wenn kein Bild
vector.DrawFilledRect(screen, 10, float32(currentY+2), 28, 28, color.RGBA{60, 60, 60, 255}, false)
}
// Text daneben
text.Draw(screen, label, basicfont.Face7x13, 45, int(currentY+float64(TextOffset)), col)
}
currentY += float64(LineHeight)
}
text.Draw(screen, "--- ASSETS ---", basicfont.Face7x13, 10, int(currentY), color.RGBA{150, 150, 150, 255})
currentY += float64(LineHeight)
for _, id := range e.sortedIDs {
col := ColText
if id == e.selectedID {
col = ColHighlight
}
// Bild holen aus e.assetsImages
img := e.assetsImages[id]
// Falls AssetDefinition sagt es gibt ein Filename, aber img nil ist, versuchen wir es via ID
if img == nil {
if def, ok := e.manifest.Assets[id]; ok && def.Filename != "" {
// ID könnte abweichen wenn Filename anders heißt, wir suchen in assetsImages nach ID
img = e.assetsImages[def.ID]
}
}
drawListItem(id, id, col, img)
}
currentY += 20
text.Draw(screen, "--- RAW FILES ---", basicfont.Face7x13, 10, int(currentY), color.RGBA{150, 150, 150, 255})
currentY += float64(LineHeight)
for _, f := range e.rawFiles {
ext := filepath.Ext(f)
id := strings.TrimSuffix(f, ext)
if _, exists := e.manifest.Assets[id]; exists {
continue
}
// Bild für Raw File
img := e.assetsImages[id]
drawListItem("+ "+f, id, ColNewFile, img)
}
// --- 2. CANVAS MITTE ---
viewX := float64(WidthList)
centerX := float64(WidthList) + float64(CanvasWidth-WidthList-WidthInspect)/2
centerY := float64(CanvasHeight) * 0.75
vector.StrokeLine(screen, float32(viewX), float32(centerY), float32(CanvasWidth-WidthInspect), float32(centerY), 2, ColAxis, false)
text.Draw(screen, "BODEN (Y=0) | [P] Player Ref Toggle", basicfont.Face7x13, int(viewX)+10, int(centerY)-10, ColAxis)
vector.StrokeLine(screen, float32(centerX), 0, float32(centerX), CanvasHeight, 1, color.RGBA{100, 100, 100, 255}, false)
// A. PLAYER GHOST
if e.showPlayerRef && e.selectedID != "player" {
if playerDef, ok := e.manifest.Assets["player"]; ok {
posX := centerX + playerDef.DrawOffX
posY := centerY + playerDef.DrawOffY
col := ColPlayerRef
if playerDef.Filename != "" {
if img := e.assetsImages[playerDef.ID]; img != nil {
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(playerDef.Scale, playerDef.Scale)
op.GeoM.Translate(posX, posY)
op.ColorScale.ScaleAlpha(0.5)
screen.DrawImage(img, op)
}
} else {
vector.DrawFilledRect(screen, float32(posX), float32(posY), float32(playerDef.ProcWidth), float32(playerDef.ProcHeight), col, false)
}
hx := float32(centerX + playerDef.DrawOffX + playerDef.Hitbox.OffsetX)
hy := float32(centerY + playerDef.DrawOffY + playerDef.Hitbox.OffsetY)
vector.StrokeRect(screen, hx, hy, float32(playerDef.Hitbox.W), float32(playerDef.Hitbox.H), 1, col, false)
}
}
// B. CURRENT OBJECT
if e.selectedID != "" {
def := e.manifest.Assets[e.selectedID]
posX := centerX + def.DrawOffX
posY := centerY + def.DrawOffY
img := e.assetsImages[def.ID]
if def.Filename != "" {
if img != nil {
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(def.Scale, def.Scale)
op.GeoM.Translate(posX, posY)
screen.DrawImage(img, op)
} else {
fallbackCol := def.Color.ToRGBA()
vector.DrawFilledRect(screen, float32(posX+def.Hitbox.OffsetX), float32(posY+def.Hitbox.OffsetY), float32(def.Hitbox.W), float32(def.Hitbox.H), fallbackCol, false)
text.Draw(screen, "MISSING IMG", basicfont.Face7x13, int(posX), int(posY), color.RGBA{255, 255, 255, 255})
}
} else {
col := def.Color.ToRGBA()
vector.DrawFilledRect(screen, float32(posX), float32(posY), float32(def.ProcWidth), float32(def.ProcHeight), col, false)
}
// Hitbox Current
hx := float32(centerX + def.DrawOffX + def.Hitbox.OffsetX)
hy := float32(centerY + def.DrawOffY + def.Hitbox.OffsetY)
vector.DrawFilledRect(screen, hx, hy, float32(def.Hitbox.W), float32(def.Hitbox.H), color.RGBA{255, 0, 0, 80}, false)
vector.StrokeRect(screen, hx, hy, float32(def.Hitbox.W), float32(def.Hitbox.H), 2, color.RGBA{255, 0, 0, 255}, false)
}
// --- 3. INSPECTOR RECHTS ---
panelX := float64(CanvasWidth - WidthInspect)
vector.DrawFilledRect(screen, float32(panelX), 0, WidthInspect, CanvasHeight, ColPanel, false)
if e.selectedID != "" {
def := e.manifest.Assets[e.selectedID]
y := 40
drawLabel := func(txt string) {
text.Draw(screen, txt, basicfont.Face7x13, int(panelX)+10, y+TextOffset, ColHighlight)
}
text.Draw(screen, "ID: "+def.ID, basicfont.Face7x13, int(panelX)+10, y+TextOffset, color.RGBA{255, 255, 255, 255})
y += HeaderHeight
drawLabel("TYPE: [Click to change]")
y += 20
vector.DrawFilledRect(screen, float32(panelX)+10, float32(y), WidthInspect-20, 20, ColInput, false)
text.Draw(screen, strings.ToUpper(def.Type), basicfont.Face7x13, int(panelX)+20, y+14, color.RGBA{255, 255, 255, 255})
y += HeaderHeight
drawVal := func(lbl string, val float64, fieldID string) {
str := fmt.Sprintf("%.2f", val)
col := color.RGBA{255, 255, 255, 255}
if e.activeField == fieldID {
str = e.inputBuffer + "_"
col = ColHighlight
}
text.Draw(screen, lbl+": "+str, basicfont.Face7x13, int(panelX)+20, y+TextOffset, col)
y += LineHeight
}
if def.Filename == "" {
drawLabel("--- PROCEDURAL ---")
y += 20
drawVal("W", def.ProcWidth, "proc_w")
drawVal("H", def.ProcHeight, "proc_h")
} else {
drawLabel("--- IMAGE ---")
y += 20
drawVal("Scale", def.Scale, "scale")
}
drawLabel("--- OFFSET (L-Click Drag) ---")
y += 20
drawVal("X", def.DrawOffX, "off_x")
drawVal("Y", def.DrawOffY, "off_y")
drawLabel("--- HITBOX (R-Click Drag) ---")
y += 20
drawVal("X", def.Hitbox.OffsetX, "hb_x")
drawVal("Y", def.Hitbox.OffsetY, "hb_y")
drawVal("W", def.Hitbox.W, "hb_w")
drawVal("H", def.Hitbox.H, "hb_h")
drawLabel("--- COLOR ---")
y += 20
drawBar := func(c color.RGBA) {
vector.DrawFilledRect(screen, float32(panelX)+10, float32(y), WidthInspect-30, 10, c, false)
y += 20
}
drawBar(color.RGBA{def.Color.R, 0, 0, 255})
drawBar(color.RGBA{0, def.Color.G, 0, 255})
drawBar(color.RGBA{0, 0, def.Color.B, 255})
vector.DrawFilledRect(screen, float32(panelX)+WidthInspect-30, float32(y-60), 20, 50, def.Color.ToRGBA(), false)
text.Draw(screen, "[Enter] Confirm | [S] Save", basicfont.Face7x13, int(panelX)+10, CanvasHeight-20, color.RGBA{100, 100, 100, 255})
}
}
func (e *Editor) Layout(w, h int) (int, int) { return CanvasWidth, CanvasHeight }
func main() {
os.MkdirAll(RawDir, 0755)
os.MkdirAll(filepath.Dir(OutFile), 0755)
ebiten.SetWindowSize(CanvasWidth, CanvasHeight)
ebiten.SetWindowTitle("Escape Prefab Editor - Pro")
if err := ebiten.RunGame(NewEditor()); err != nil {
log.Fatal(err)
}
}