aboutsummaryrefslogtreecommitdiff
path: root/model.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-23 04:38:06 -0400
committerhistoria <[not public]>2026-06-23 04:38:06 -0400
commitb8e473b069d9c81e2f6a471fa7636c27cee63fae (patch)
tree1cc5271e670fd922cb6a64e5fd8d7d9478ae2aec /model.go
downloadtui-ascii-mapper-b8e473b069d9c81e2f6a471fa7636c27cee63fae.tar.gz
initial commit
Diffstat (limited to 'model.go')
-rw-r--r--model.go267
1 files changed, 267 insertions, 0 deletions
diff --git a/model.go b/model.go
new file mode 100644
index 0000000..99a08be
--- /dev/null
+++ b/model.go
@@ -0,0 +1,267 @@
+package main
+
+import (
+ "fmt"
+ "math/rand"
+)
+
+type Point struct{ X, Y int }
+
+type TerrainColor struct {
+ Color string `yaml:"color"`
+ Weight int `yaml:"weight"`
+}
+
+type Terrain struct {
+ Name string `yaml:"name"`
+ Symbol string `yaml:"symbol"`
+ ASCII string `yaml:"ascii"`
+ Colors []TerrainColor `yaml:"colors"`
+}
+
+func (t Terrain) GetSymbol(unicode bool) string {
+ if unicode {
+ return t.Symbol
+ }
+ return t.ASCII
+}
+
+func (t Terrain) PickColor() string {
+ if len(t.Colors) == 0 {
+ return "0"
+ }
+ if len(t.Colors) == 1 {
+ return t.Colors[0].Color
+ }
+ total := 0
+ for _, c := range t.Colors {
+ total += c.Weight
+ }
+ r := rand.Intn(total)
+ for _, c := range t.Colors {
+ r -= c.Weight
+ if r < 0 {
+ return c.Color
+ }
+ }
+ return t.Colors[0].Color
+}
+
+type Cell struct {
+ Terrain int `yaml:"t"`
+ Color string `yaml:"c,omitempty"` // persisted ANSI color — set when drawn, stays fixed
+ Text string `yaml:"x,omitempty"`
+}
+
+type TextLabel struct {
+ Text string `yaml:"text"`
+ Start Point `yaml:"start"`
+ Color string `yaml:"color,omitempty"`
+}
+
+type Map struct {
+ Name string `yaml:"name"`
+ Width int `yaml:"width"`
+ Height int `yaml:"height"`
+ Grid [][]Cell `yaml:"-"`
+ Palette []Terrain `yaml:"palette"`
+ TextLabels []TextLabel `yaml:"text_labels,omitempty"`
+ Submaps map[Point]*Map `yaml:"-"`
+ Parent *Map `yaml:"-"`
+ Filename string `yaml:"-"`
+}
+
+func NewMap(name string, w, h int, palette []Terrain) *Map {
+ grid := make([][]Cell, h)
+ for y := range grid {
+ grid[y] = make([]Cell, w)
+ for x := range grid[y] {
+ grid[y][x].Terrain = -1
+ }
+ }
+ return &Map{
+ Name: name,
+ Width: w,
+ Height: h,
+ Grid: grid,
+ Palette: palette,
+ Submaps: make(map[Point]*Map),
+ }
+}
+
+func (m *Map) Clone() *Map {
+ grid := make([][]Cell, m.Height)
+ for y := range grid {
+ grid[y] = make([]Cell, m.Width)
+ copy(grid[y], m.Grid[y])
+ }
+ c := &Map{
+ Name: m.Name,
+ Width: m.Width,
+ Height: m.Height,
+ Grid: grid,
+ Palette: m.Palette,
+ Submaps: make(map[Point]*Map),
+ Parent: m.Parent,
+ }
+ for _, tl := range m.TextLabels {
+ c.TextLabels = append(c.TextLabels, tl)
+ }
+ for pt, sub := range m.Submaps {
+ c.Submaps[pt] = sub
+ }
+ return c
+}
+
+func (m *Map) InBounds(p Point) bool {
+ return p.X >= 0 && p.X < m.Width && p.Y >= 0 && p.Y < m.Height
+}
+
+func (m *Map) SetCell(p Point, terrain int, color ...string) {
+ if !m.InBounds(p) {
+ return
+ }
+ text := m.Grid[p.Y][p.X].Text
+ clr := ""
+ if len(color) > 0 {
+ clr = color[0]
+ }
+ m.Grid[p.Y][p.X] = Cell{Terrain: terrain, Color: clr, Text: text}
+}
+
+func (m *Map) SetText(p Point, text string) {
+ if !m.InBounds(p) {
+ return
+ }
+ m.Grid[p.Y][p.X].Text = text
+}
+
+func (m *Map) CellAt(p Point) Cell {
+ if !m.InBounds(p) {
+ return Cell{Terrain: -1}
+ }
+ return m.Grid[p.Y][p.X]
+}
+
+type UndoStack struct {
+ states []undoEntry
+ pos int
+}
+
+type undoEntry struct {
+ target *Map
+ state *Map
+}
+
+func (u *UndoStack) Push(target *Map) {
+ keep := u.pos + 1
+ if keep > len(u.states) {
+ keep = len(u.states)
+ }
+ u.states = append(u.states[:keep], undoEntry{target: target, state: target.Clone()})
+ u.pos = len(u.states) - 1
+ if len(u.states) > 100 {
+ u.states = u.states[1:]
+ u.pos--
+ }
+}
+
+func (u *UndoStack) Undo() *undoEntry {
+ if u.pos <= 0 {
+ return nil
+ }
+ u.pos--
+ return &u.states[u.pos]
+}
+
+func (u *UndoStack) Redo() *undoEntry {
+ if u.pos >= len(u.states)-1 {
+ return nil
+ }
+ u.pos++
+ return &u.states[u.pos]
+}
+
+type Tool int
+
+const (
+ ToolBrush Tool = iota
+ ToolSelect
+ ToolErase
+ ToolFill
+ ToolLine
+ ToolRect
+ ToolCircle
+ ToolText
+)
+
+func (t Tool) String() string {
+ switch t {
+ case ToolBrush:
+ return "Brush"
+ case ToolSelect:
+ return "Select"
+ case ToolErase:
+ return "Erase"
+ case ToolFill:
+ return "Fill"
+ case ToolLine:
+ return "Line"
+ case ToolRect:
+ return "Rect"
+ case ToolCircle:
+ return "Circle"
+ case ToolText:
+ return "Text"
+ }
+ return ""
+}
+
+type Mode int
+
+const (
+ ModeNormal Mode = iota
+ ModeDialog
+ ModeLinePreview
+ ModeRectPreview
+ ModeCirclePreview
+ ModeTextEdit
+)
+
+type DialogType int
+
+const (
+ DialogNone DialogType = iota
+ DialogSaveAs
+ DialogOpenMap
+ DialogResize
+ DialogQuitConfirm
+ DialogDeleteSubmapConfirm
+ DialogRenameSymbol
+ DialogRenameMap
+ DialogFileSave
+ DialogFileOpen
+)
+
+func clamp(v, lo, hi int) int {
+ if v < lo {
+ return lo
+ }
+ if v > hi {
+ return hi
+ }
+ return v
+}
+
+func DemoModel() {
+ m := NewMap("test", 10, 5, nil)
+ if m.Width != 10 || m.Height != 5 || m.Grid[0][0].Terrain != -1 {
+ panic("NewMap broken")
+ }
+ m2 := m.Clone()
+ m2.Grid[0][0].Terrain = 0
+ if m.Grid[0][0].Terrain != -1 {
+ panic("Clone shares data")
+ }
+ fmt.Println("model: ok")
+}