add documentation

This commit is contained in:
2026-05-25 20:03:49 +02:00
parent f0b86871f6
commit e4a7cc9177
4 changed files with 417 additions and 876 deletions

193
README.md Normal file
View File

@@ -0,0 +1,193 @@
# pncdsl
A point-and-click adventure game framework for Go, built on top of
[Ebitengine](https://ebitengine.org/). You describe your game with
**declarative struct literals**: every entity (item, scene, character,
dialogue, script, asset, verb) is registered through its matching
`XxxManager.Register(...)` call, and the library handles the rest —
input, rendering, state, dialog trees, cutscenes.
## What it is
Inspired by the classic LucasArts SCUMM era (Maniac Mansion, Monkey
Island, Day of the Tentacle). The framework doesn't try to be a generic
"engine" — it's a thin Go-level DSL layer on top of Ebitengine that gives
you the fixed skeleton of the adventure genre:
- scenes with hotspots and click zones,
- a verb-bar UI: Look / Use / Talk / Take,
- inventory, item-on-item and item-on-hotspot interactions,
- dialog trees with conditional choices,
- a script/cutscene system with composite actions (`Seq`, `Par`, `If`, `Wait`),
- world state (flags, vars), validation.
The approach deliberately collapses to **one single pattern**: every
piece is registered via `Manager.Register(Entity{Name: "..."})`. No
builders, no fluent chains, no registry sprawl.
## Quick start
Prerequisites: Go 1.24+ (for generic type aliases) and a working OpenGL
context.
```bash
git clone <repo>
cd pncdsl
go run .
```
Opens an Ebiten window at 1280×800 (internal resolution 320×200, scaled
4×). No asset files are required — the library generates deterministic
colored placeholders for anything missing under `assets/`.
## One-minute example
```go
// main.go
package main
import (
"log"
"pncdsl/pncdsl"
)
func main() {
g := pncdsl.NewGame("Sample", 320, 200)
g.AssetManager.Register(pncdsl.Asset{Name: "bg/kitchen", Path: "assets/bg/kitchen.png", Kind: pncdsl.AssetImage})
g.ItemManager.Register(pncdsl.Item{
Name: "key", Sprite: "spr/key", Description: "a rusty key",
})
g.SceneManager.Register(pncdsl.Scene{
Name: "kitchen",
Background: "bg/kitchen",
Hotspots: []pncdsl.Hotspot{
{
Name: "drawer",
Area: pncdsl.Rect(40, 80, 60, 40),
Label: "drawer",
OnLook: pncdsl.Say("player", "A drawer. Wonder what's inside?"),
OnUse: pncdsl.Seq(pncdsl.Give("key"), pncdsl.Say("player", "A key!")),
},
},
})
g.CharacterManager.Register(pncdsl.Character{Name: "player", W: 28, H: 62})
g.StartAt("kitchen")
if err := pncdsl.Run(g); err != nil {
log.Fatal(err)
}
}
```
## The central pattern — `Manager`
Every name-addressable entity goes into the game through one shared type:
```go
type Named interface { GetName() string }
type Manager[T Named] struct { /* ... */ }
func (m *Manager[T]) Register(v T)
func (m *Manager[T]) Get(name string) (T, bool)
func (m *Manager[T]) MustGet(name string) T
func (m *Manager[T]) Has(name string) bool
func (m *Manager[T]) Names() []string
```
Each entity kind gets a generic alias (`ItemManager = Manager[Item]`,
`SceneManager = Manager[Scene]`, …) so usage is uniform:
```go
g.ItemManager.Register(pncdsl.Item{Name: "key", ...})
g.SceneManager.Register(pncdsl.Scene{Name: "kitchen", ...})
g.AssetManager.Register(pncdsl.Asset{Name: "bg/kitchen", Path: "..."})
```
A duplicate or empty `Name` panics — that's a construction-time bug,
not a runtime error.
Full design rationale: [`PLAN.md`](PLAN.md).
## File layout
```
pncdsl/
├── main.go # entry point: pncdsl.Run(domain.Build())
├── pncdsl/ # the library — theme-prefixed file names
│ ├── core.*.go # Game, engine, manager, dsl, errors
│ ├── scene.*.go # Scene, Hotspot, Trigger, Camera, Transition
│ ├── item.*.go # Item, Inventory
│ ├── actor.*.go # Character, Animation
│ ├── dialog.*.go # Dialogue, DialogBox
│ ├── action.*.go # Action/Runner, built-in actions, Condition
│ ├── state.*.go # State, Save (stub)
│ ├── ui.*.go # VerbBar, Cursor, SpeechBubble
│ ├── asset.*.go # Registry, lazy load, Audio (stub), Text
│ └── util.*.go # geometry, timer, log
├── domain/ # the concrete game — one file per entity
│ ├── game.go # Build()
│ ├── scene.bedroom.go # ↔ g.SceneManager.Register(...Name: "bedroom"...)
│ ├── item.key.go # ↔ g.ItemManager.Register(...Name: "key"...)
│ └── ...
├── PLAN.md # detailed design document
├── DEMO.md # demo game overview ◀────────
├── GFX.md # asset prompts for image-AI
└── README.md # this file
```
Files under `domain/` always follow `theme.identifier.go` (e.g.
`scene.kitchen.go`, `character.player.go`) — `ls domain/scene.*`
instantly lists every location.
## The demo
The repo ships with a mini-game (`domain/`) titled **"Morning Coffee"**.
Two scenes, three items, one NPC with a dialog, one intro and one ending
cutscene — just enough to exercise every library feature without the
demo outgrowing the library.
Full walkthrough and file mapping: [`DEMO.md`](DEMO.md).
If you want to generate art for the game, prompts for image AIs are in
[`GFX.md`](GFX.md).
## Current status
| Area | State |
|-------------------------------|----------------------------------------------------------|
| Manager registries | ✅ complete (Item, Scene, Character, Dialogue, Script, Asset, Verb) |
| Hotspot + verb interaction | ✅ |
| Inventory, use-with-item | ✅ (both `hotspot.OnUseWith` and `item.OnUseWith`) |
| Dialog tree + conditional choices | ✅ (`Show` condition, `Once` flag) |
| `Action`/`Runner` engine | ✅ Seq / Par / If / Wait / Say / Walk / Give / GoTo / … |
| Condition library | ✅ Flag, HasItem, SelectedItem, InScene, VarEq, Not/And/Or |
| Scene transitions (fade) | ✅ on scene change |
| Asset placeholders | ✅ deterministic color for missing files |
| Stylized character placeholder | ✅ humanoid / quadruped shape when no sprite art |
| Audio (PlayMusic/PlaySound) | 🟡 log-only — Ebiten audio backend not wired up yet |
| Animation (sprite sheet) | 🟡 `AnimationClip` field exists, rendering not yet |
| Walkbox + A\* | 🟡 straight-line movement only |
| Save / Load (JSON) | 🟡 API in place, implementation stubbed |
| Trigger processing | 🟡 struct exists, engine doesn't run them yet |
## Testing
```bash
go test ./...
```
`domain/build_test.go` is a headless smoke test: it calls `Build()` and
then `Validate()` to cross-check every name reference between managers.
No Ebiten window is opened.
## Further reading
- [`DEMO.md`](DEMO.md) — walkthrough of the "Morning Coffee" demo
## License
MIT — see [`LICENSE.md`](LICENSE.md). Copyright © 2026 Teletype Games.