diff --git a/assets/bg/bedroom.png b/assets/bg/bedroom.png new file mode 100644 index 0000000..5f30155 Binary files /dev/null and b/assets/bg/bedroom.png differ diff --git a/assets/bg/kitchen.png b/assets/bg/kitchen.png new file mode 100644 index 0000000..bd3d5b3 Binary files /dev/null and b/assets/bg/kitchen.png differ diff --git a/assets/spr/player.png b/assets/spr/player.png new file mode 100644 index 0000000..2d85d45 Binary files /dev/null and b/assets/spr/player.png differ diff --git a/domain/character.cat.go b/domain/character.cat.go index 04945f7..2e075f7 100644 --- a/domain/character.cat.go +++ b/domain/character.cat.go @@ -12,7 +12,7 @@ func defineCat(g *p.Game) { Sprite: "spr/cat", Speed: 40, SpeechColor: color.RGBA{200, 200, 255, 255}, - W: 18, - H: 14, + W: 34, + H: 20, }) } diff --git a/domain/character.player.go b/domain/character.player.go index 0699e78..be4e9a5 100644 --- a/domain/character.player.go +++ b/domain/character.player.go @@ -12,7 +12,7 @@ func definePlayer(g *p.Game) { Sprite: "spr/player", Speed: 80, SpeechColor: color.RGBA{255, 230, 160, 255}, - W: 14, - H: 28, + W: 28, + H: 62, }) } diff --git a/domain/scene.bedroom.go b/domain/scene.bedroom.go index 5dc0d8e..58a1329 100644 --- a/domain/scene.bedroom.go +++ b/domain/scene.bedroom.go @@ -8,7 +8,7 @@ func defineBedroom(g *p.Game) { Background: "bg/bedroom", Music: "mus/wakeup", Actors: []p.SceneActor{ - {CharacterName: "player", At: p.Point{X: 160, Y: 120}}, + {CharacterName: "player", At: p.Point{X: 160, Y: 145}}, }, Hotspots: []p.Hotspot{ { @@ -29,7 +29,7 @@ func defineBedroom(g *p.Game) { OnTake: p.If(p.Flag(FlagKeyTaken), p.Say("player", "Már elvittem."), p.Seq( - p.Walk("player", p.Point{X: 130, Y: 120}), + p.Walk("player", p.Point{X: 130, Y: 145}), p.Give("key"), p.SetFlag(FlagKeyTaken), p.Say("player", "Pont jól jöhet."), @@ -38,7 +38,7 @@ func defineBedroom(g *p.Game) { OnUse: p.If(p.Flag(FlagKeyTaken), p.Say("player", "Már elvittem a kulcsot."), p.Seq( - p.Walk("player", p.Point{X: 130, Y: 120}), + p.Walk("player", p.Point{X: 130, Y: 145}), p.Give("key"), p.SetFlag(FlagKeyTaken), p.Say("player", "Megvan a kulcs."), @@ -51,7 +51,7 @@ func defineBedroom(g *p.Game) { Label: "ajtó", OnLook: p.Say("player", "Az ajtó a konyhába vezet."), OnUse: p.Seq( - p.Walk("player", p.Point{X: 260, Y: 130}), + p.Walk("player", p.Point{X: 260, Y: 145}), p.GoTo("kitchen"), ), }, diff --git a/domain/scene.kitchen.go b/domain/scene.kitchen.go index ef598b1..d249437 100644 --- a/domain/scene.kitchen.go +++ b/domain/scene.kitchen.go @@ -8,8 +8,8 @@ func defineKitchen(g *p.Game) { Background: "bg/kitchen", Music: "mus/calm", Actors: []p.SceneActor{ - {CharacterName: "player", At: p.Point{X: 60, Y: 130}}, - {CharacterName: "cat", At: p.Point{X: 210, Y: 132}}, + {CharacterName: "player", At: p.Point{X: 60, Y: 145}}, + {CharacterName: "cat", At: p.Point{X: 215, Y: 142}}, }, Hotspots: []p.Hotspot{ { @@ -21,7 +21,7 @@ func defineKitchen(g *p.Game) { p.Say("player", "Már nyitva van."), p.If(p.HasItem("key"), p.Seq( - p.Walk("player", p.Point{X: 70, Y: 130}), + p.Walk("player", p.Point{X: 70, Y: 145}), p.Say("player", "Klikk."), p.SetFlag(FlagCupboardOpen), p.Give("beans"), @@ -32,7 +32,7 @@ func defineKitchen(g *p.Game) { ), OnUseWith: map[string]p.Action{ "key": p.Seq( - p.Walk("player", p.Point{X: 70, Y: 130}), + p.Walk("player", p.Point{X: 70, Y: 145}), p.Say("player", "Klikk."), p.SetFlag(FlagCupboardOpen), p.Give("beans"), @@ -48,7 +48,7 @@ func defineKitchen(g *p.Game) { OnTake: p.If(p.Flag(FlagMugTaken), p.Say("player", "Tiszta már mind elfogyott."), p.Seq( - p.Walk("player", p.Point{X: 125, Y: 130}), + p.Walk("player", p.Point{X: 125, Y: 145}), p.Give("mug"), p.SetFlag(FlagMugTaken), p.Say("player", "Egy bögre, kérem."), @@ -57,7 +57,7 @@ func defineKitchen(g *p.Game) { OnUse: p.If(p.Flag(FlagMugTaken), p.Say("player", "Már elvettem egyet."), p.Seq( - p.Walk("player", p.Point{X: 125, Y: 130}), + p.Walk("player", p.Point{X: 125, Y: 145}), p.Give("mug"), p.SetFlag(FlagMugTaken), p.Say("player", "Egy bögre, kérem."), @@ -75,7 +75,7 @@ func defineKitchen(g *p.Game) { ), OnUseWith: map[string]p.Action{ "beans": p.Seq( - p.Walk("player", p.Point{X: 180, Y: 130}), + p.Walk("player", p.Point{X: 180, Y: 145}), p.SetFlag(FlagCoffeeHasBeans), p.TakeAway("beans"), p.Say("player", "Bab a helyén."), @@ -83,7 +83,7 @@ func defineKitchen(g *p.Game) { p.RunScript("victory")), ), "mug": p.Seq( - p.Walk("player", p.Point{X: 180, Y: 130}), + p.Walk("player", p.Point{X: 180, Y: 145}), p.SetFlag(FlagCoffeeHasMug), p.TakeAway("mug"), p.Say("player", "Bögre a helyén."), @@ -105,7 +105,7 @@ func defineKitchen(g *p.Game) { Label: "ajtó", OnLook: p.Say("player", "Vissza a hálóba."), OnUse: p.Seq( - p.Walk("player", p.Point{X: 20, Y: 130}), + p.Walk("player", p.Point{X: 20, Y: 145}), p.GoTo("bedroom"), ), }, diff --git a/domain/script.intro.go b/domain/script.intro.go index 72d0f1e..944e7bc 100644 --- a/domain/script.intro.go +++ b/domain/script.intro.go @@ -9,7 +9,7 @@ func defineIntroScript(g *p.Game) { p.Wait(0.4), p.Say("player", "Brr, hideg van."), p.Say("player", "Egy kávé kéne, mielőtt szétszakad a fejem."), - p.Walk("player", p.Point{X: 160, Y: 130}), + p.Walk("player", p.Point{X: 200, Y: 145}), ), }) } diff --git a/pncdsl/asset.manager.go b/pncdsl/asset.manager.go index 9171c3f..5ae9c1c 100644 --- a/pncdsl/asset.manager.go +++ b/pncdsl/asset.manager.go @@ -17,15 +17,17 @@ type AssetManager = Manager[Asset] // The Asset structs in the manager remain immutable spec; this is where // the actual *ebiten.Image bytes live, lazily decoded on first access. type loadedAssets struct { - images map[string]*ebiten.Image + images map[string]*ebiten.Image + placeholders map[string]bool defaultW, defaultH int } func newLoadedAssets(w, h int) *loadedAssets { return &loadedAssets{ - images: make(map[string]*ebiten.Image), - defaultW: w, - defaultH: h, + images: make(map[string]*ebiten.Image), + placeholders: make(map[string]bool), + defaultW: w, + defaultH: h, } } @@ -37,16 +39,23 @@ func (la *loadedAssets) image(am *AssetManager, name string) *ebiten.Image { if !ok { img := placeholderImage(name, la.defaultW, la.defaultH) la.images[name] = img + la.placeholders[name] = true return img } img := loadImageFile(a.Path) if img == nil { img = placeholderImage(name, la.defaultW, la.defaultH) + la.placeholders[name] = true } la.images[name] = img return img } +func (la *loadedAssets) isPlaceholder(name string) bool { + _, _ = la.images[name], la.placeholders[name] // ensure populated via image() + return la.placeholders[name] +} + func loadImageFile(path string) *ebiten.Image { f, err := os.Open(path) if err != nil { diff --git a/pncdsl/core.engine.go b/pncdsl/core.engine.go index f661cc4..4b53dc7 100644 --- a/pncdsl/core.engine.go +++ b/pncdsl/core.engine.go @@ -268,12 +268,14 @@ func drawCharacter(dst *ebiten.Image, g *Game, c *runtimeChar) { w := c.def.W h := c.def.H if w == 0 { - w = 14 + w = 24 } if h == 0 { - h = 26 + h = 56 } - if c.def.Sprite != "" { + // Try the real sprite first. Only fall through to the stylized + // placeholder when no actual art is on disk. + if c.def.Sprite != "" && !g.loaded.isPlaceholder(c.def.Sprite) { img := g.loaded.image(g.AssetManager, c.def.Sprite) sw, sh := img.Bounds().Dx(), img.Bounds().Dy() if sw > 0 && sh > 0 { @@ -284,9 +286,84 @@ func drawCharacter(dst *ebiten.Image, g *Game, c *runtimeChar) { return } } - col := color.RGBA{200, 200, 200, 255} - if rgba, ok := c.def.SpeechColor.(color.RGBA); ok { - col = rgba + // Touch the asset cache so isPlaceholder is populated for later frames. + if c.def.Sprite != "" { + _ = g.loaded.image(g.AssetManager, c.def.Sprite) + } + x := c.pos.X - w/2 + y := c.pos.Y - h + bodyCol := color.RGBA{200, 200, 200, 255} + if rgba, ok := c.def.SpeechColor.(color.RGBA); ok { + bodyCol = rgba + } + if w > h { + drawQuadrupedPlaceholder(dst, x, y, w, h, bodyCol) + } else { + drawHumanoidPlaceholder(dst, x, y, w, h, bodyCol) } - vector.DrawFilledRect(dst, float32(c.pos.X-w/2), float32(c.pos.Y-h), float32(w), float32(h), col, false) +} + +func drawHumanoidPlaceholder(dst *ebiten.Image, x, y, w, h float64, body color.RGBA) { + pants := color.RGBA{40, 50, 90, 255} + skin := color.RGBA{245, 200, 155, 255} + outline := color.RGBA{20, 20, 30, 255} + + headR := h * 0.14 + headCY := y + headR + 1 + + // pants / legs — two narrow strips + pantsTop := y + h*0.62 + pantsH := h - (pantsTop - y) - 1 + legW := w * 0.36 + vector.DrawFilledRect(dst, float32(x+w*0.08), float32(pantsTop), float32(legW), float32(pantsH), pants, false) + vector.DrawFilledRect(dst, float32(x+w*0.56), float32(pantsTop), float32(legW), float32(pantsH), pants, false) + + // torso — full-width rect, shirt color + torsoTop := headCY + headR + torsoH := pantsTop - torsoTop + vector.DrawFilledRect(dst, float32(x), float32(torsoTop), float32(w), float32(torsoH), body, false) + vector.StrokeRect(dst, float32(x), float32(torsoTop), float32(w), float32(torsoH), 1, outline, false) + + // head — circle in skin tone + vector.DrawFilledCircle(dst, float32(x+w/2), float32(headCY), float32(headR), skin, true) + + // feet — short dark blobs at the very bottom + footH := 2.0 + vector.DrawFilledRect(dst, float32(x+w*0.05), float32(y+h-footH), float32(legW+2), float32(footH), outline, false) + vector.DrawFilledRect(dst, float32(x+w*0.55), float32(y+h-footH), float32(legW+2), float32(footH), outline, false) +} + +func drawQuadrupedPlaceholder(dst *ebiten.Image, x, y, w, h float64, body color.RGBA) { + outline := color.RGBA{20, 20, 30, 255} + dark := color.RGBA{ + R: body.R / 2, G: body.G / 2, B: body.B / 2, A: 255, + } + + // body + bodyY := y + h*0.30 + bodyH := h * 0.65 + vector.DrawFilledRect(dst, float32(x), float32(bodyY), float32(w*0.78), float32(bodyH), body, false) + + // head — circle on the right + headR := h * 0.30 + headCX := x + w - headR + headCY := y + h*0.50 + vector.DrawFilledCircle(dst, float32(headCX), float32(headCY), float32(headR), body, true) + + // ears — two triangles approximated as small rects + earW := w * 0.07 + earH := h * 0.30 + vector.DrawFilledRect(dst, float32(headCX-headR*0.7), float32(y), float32(earW), float32(earH), body, false) + vector.DrawFilledRect(dst, float32(headCX+headR*0.4), float32(y), float32(earW), float32(earH), body, false) + + // eye — dark dot + vector.DrawFilledCircle(dst, float32(headCX+headR*0.25), float32(headCY-1), 1.2, outline, true) + + // tail — small dark line stub on the left + vector.DrawFilledRect(dst, float32(x-w*0.04), float32(bodyY+1), float32(w*0.08), 2, dark, false) + + // legs — two short bars at the bottom + legW := w * 0.07 + vector.DrawFilledRect(dst, float32(x+w*0.10), float32(y+h-3), float32(legW), 3, dark, false) + vector.DrawFilledRect(dst, float32(x+w*0.55), float32(y+h-3), float32(legW), 3, dark, false) }