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") }