diff --git a/assets/sprites/flood-tool.png b/assets/sprites/flood-tool.png new file mode 100644 index 0000000..0a44c89 Binary files /dev/null and b/assets/sprites/flood-tool.png differ diff --git a/dev-assets/doodads/azulian/azulian.js b/dev-assets/doodads/azulian/azulian.js index 5b580ce..2c77b6d 100644 --- a/dev-assets/doodads/azulian/azulian.js +++ b/dev-assets/doodads/azulian/azulian.js @@ -4,7 +4,7 @@ const color = Self.GetTag("color"); var playerSpeed = color === 'blue' ? 2 : 4, aggroX = 250, // X/Y distance sensitivity from player aggroY = color === 'blue' ? 100 : 200, - jumpSpeed = color === 'blue' ? 12 : 14, + jumpSpeed = color === 'blue' ? 14 : 18, animating = false, direction = "right", lastDirection = "right"; @@ -14,7 +14,7 @@ if (color === 'white') { aggroX = 1000; aggroY = 400; playerSpeed = 8; - jumpSpeed = 16; + jumpSpeed = 20; } function setupAnimations(color) { diff --git a/pkg/balance/cheats.go b/pkg/balance/cheats.go index ca7be9a..eadeef2 100644 --- a/pkg/balance/cheats.go +++ b/pkg/balance/cheats.go @@ -32,4 +32,10 @@ var ( CheatPlayAsAnvil = "megaton weight" CheatGodMode = "god mode" CheatDebugLoadScreen = "test load screen" + CheatUnlockLevels = "master key" +) + +// Global cheat boolean states. +var ( + CheatEnabledUnlockLevels bool ) diff --git a/pkg/balance/feature_flags.go b/pkg/balance/feature_flags.go index e00fc5b..f1a8510 100644 --- a/pkg/balance/feature_flags.go +++ b/pkg/balance/feature_flags.go @@ -4,8 +4,7 @@ package balance var Feature = feature{ ///////// // Experimental features that are off by default - Zoom: false, // enable the zoom in/out feature (very buggy rn) - ChangePalette: false, // reset your palette after level creation to a diff preset + ViewportWindow: false, // Open new viewport into your level ///////// // Fully activated features @@ -15,12 +14,17 @@ var Feature = feature{ // Allow embedded doodads in levels. EmbeddableDoodads: true, + + // Enable the zoom in/out feature (kinda buggy still) + Zoom: true, + + // Reassign an existing level's palette to a different builtin. + ChangePalette: true, } // FeaturesOn turns on all feature flags, from CLI --experimental option. func FeaturesOn() { - Feature.Zoom = true - Feature.ChangePalette = true + Feature.ViewportWindow = true } type feature struct { @@ -28,4 +32,5 @@ type feature struct { CustomWallpaper bool ChangePalette bool EmbeddableDoodads bool + ViewportWindow bool } diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index 25c441a..cfbbead 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -101,6 +101,10 @@ var ( // GameController thresholds. GameControllerMouseMoveMax float64 = 20 // Max pixels per tick to simulate mouse movement. GameControllerScrollMin float64 = 0.3 // Minimum threshold for a right-stick scroll event. + + // Limits on the Flood Fill tool so it doesn't run away on us. + FloodToolVoidLimit = 600 // If clicking the void, +- 1000 px limit + FloodToolLimit = 1200 // If clicking a valid color on the level ) // Edit Mode Values diff --git a/pkg/cheats.go b/pkg/cheats.go index f00eff0..efdce7c 100644 --- a/pkg/cheats.go +++ b/pkg/cheats.go @@ -161,6 +161,14 @@ func (c Command) cheatCommand(d *Doodle) bool { loadscreen.Hide() }() + case balance.CheatUnlockLevels: + balance.CheatEnabledUnlockLevels = !balance.CheatEnabledUnlockLevels + if balance.CheatEnabledUnlockLevels { + d.Flash("All locked Story Mode levels can now be played.") + } else { + d.Flash("All locked Story Mode levels are again locked.") + } + default: return false } diff --git a/pkg/drawtool/tools.go b/pkg/drawtool/tools.go index 83acd49..4e5d0da 100644 --- a/pkg/drawtool/tools.go +++ b/pkg/drawtool/tools.go @@ -14,6 +14,7 @@ const ( EraserTool PanTool TextTool + FloodTool ) var toolNames = []string{ @@ -26,6 +27,7 @@ var toolNames = []string{ "Eraser", "PanTool", "TextTool", + "FloodTool", } func (t Tool) String() string { diff --git a/pkg/editor_ui_menubar.go b/pkg/editor_ui_menubar.go index 8d0c2a5..fd87bbc 100644 --- a/pkg/editor_ui_menubar.go +++ b/pkg/editor_ui_menubar.go @@ -5,13 +5,13 @@ package doodle // The rest of it is controlled in editor_ui.go import ( + "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/level/giant_screenshot" "git.kirsle.net/apps/doodle/pkg/license" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/native" - "git.kirsle.net/apps/doodle/pkg/usercfg" "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/pkg/windows" "git.kirsle.net/go/render" @@ -137,7 +137,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { native.OpenLocalURL(userdir.ScreenshotDirectory) }) - if usercfg.Current.EnableFeatures { + if balance.Feature.ViewportWindow { levelMenu.AddSeparator() levelMenu.AddItemAccel("New viewport", "v", func() { pip := windows.MakePiPWindow(d.width, d.height, windows.PiP{ diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index f5d935e..9744ba2 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -298,6 +298,10 @@ func (u *EditorUI) SetupPopups(d *Doodle) { u.Scene.Doodad.Layers[u.Scene.ActiveLayer].Chunker.Redraw() } + // Flush the palette cache in case swatches got renamed, + // so it rebuilds the "color by name" map from scratch. + pal.FlushCaches() + // Reload the palette frame to reflect the changed data. u.Palette.Hide() u.Palette = u.SetupPalette(d) diff --git a/pkg/editor_ui_toolbar.go b/pkg/editor_ui_toolbar.go index 0dc3ab2..550ce8b 100644 --- a/pkg/editor_ui_toolbar.go +++ b/pkg/editor_ui_toolbar.go @@ -154,6 +154,35 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { }, }, + { + Value: drawtool.FloodTool.String(), + Icon: "assets/sprites/flood-tool.png", + Tooltip: "Flood Tool", + Click: func() { + u.Canvas.Tool = drawtool.FloodTool + d.Flash("Flood Tool selected.") + }, + }, + + { + Value: drawtool.EraserTool.String(), + Icon: "assets/sprites/eraser-tool.png", + Tooltip: "Eraser Tool", + Style: &balance.ButtonLightRed, + Click: func() { + u.Canvas.Tool = drawtool.EraserTool + + // Set the brush size within range for the eraser. + if u.Canvas.BrushSize < balance.DefaultEraserBrushSize { + u.Canvas.BrushSize = balance.DefaultEraserBrushSize + } else if u.Canvas.BrushSize > balance.MaxEraserBrushSize { + u.Canvas.BrushSize = balance.MaxEraserBrushSize + } + + d.Flash("Eraser Tool selected.") + }, + }, + { Value: drawtool.ActorTool.String(), Icon: "assets/sprites/actor-tool.png", @@ -178,25 +207,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { d.Flash("Link Tool selected. Click a doodad in your level to link it to another.") }, }, - - { - Value: drawtool.EraserTool.String(), - Icon: "assets/sprites/eraser-tool.png", - Tooltip: "Eraser Tool", - Style: &balance.ButtonLightRed, - Click: func() { - u.Canvas.Tool = drawtool.EraserTool - - // Set the brush size within range for the eraser. - if u.Canvas.BrushSize < balance.DefaultEraserBrushSize { - u.Canvas.BrushSize = balance.DefaultEraserBrushSize - } else if u.Canvas.BrushSize > balance.MaxEraserBrushSize { - u.Canvas.BrushSize = balance.MaxEraserBrushSize - } - - d.Flash("Eraser Tool selected.") - }, - }, } // Arrange the buttons 2x2. diff --git a/pkg/enum/enum.go b/pkg/enum/enum.go index ece7391..79e09a0 100644 --- a/pkg/enum/enum.go +++ b/pkg/enum/enum.go @@ -38,8 +38,8 @@ const ( func (d Difficulty) String() string { return []string{ + "Peaceful", "Normal", "Hard", - "Peaceful", - }[d] + }[d+1] } diff --git a/pkg/gamepad/gamepad.go b/pkg/gamepad/gamepad.go index a84f7b9..0e2c492 100644 --- a/pkg/gamepad/gamepad.go +++ b/pkg/gamepad/gamepad.go @@ -125,7 +125,7 @@ func Loop(ev *event.State) { if len(ev.Controllers) > 0 { for idx, ctrl := range ev.Controllers { SetControllerIndex(idx) - log.Info("gamepad: using controller #%d (%s) as Player 1", idx, ctrl) + log.Info("Gamepad: using controller #%d (%d) as Player 1", idx, ctrl.Name()) break } } else { diff --git a/pkg/keybind/keybind.go b/pkg/keybind/keybind.go index 771484b..aa4456f 100644 --- a/pkg/keybind/keybind.go +++ b/pkg/keybind/keybind.go @@ -276,7 +276,17 @@ func Use(ev *event.State) bool { return ev.Space || ev.KeyDown("q") } +// LeftClick of the primary mouse button. +func LeftClick(ev *event.State) bool { + return ev.Button1 +} + // MiddleClick of the mouse for panning the level. func MiddleClick(ev *event.State) bool { return ev.Button2 } + +// ClearLeftClick sets the primary mouse button state to false. +func ClearLeftClick(ev *event.State) { + ev.Button1 = false +} diff --git a/pkg/level/palette.go b/pkg/level/palette.go index 6dc8b9a..5264135 100644 --- a/pkg/level/palette.go +++ b/pkg/level/palette.go @@ -90,6 +90,14 @@ func (p *Palette) Inflate() { p.update() } +// FlushCaches if you have modified the swatches, especially if you have +// changed the name of an existing color. This invalidates the "by name" +// cache and rebuilds it from scratch. +func (p *Palette) FlushCaches() { + p.byName = nil + p.update() +} + // AddSwatch adds a new swatch to the palette. func (p *Palette) AddSwatch() *Swatch { p.update() diff --git a/pkg/level/types.go b/pkg/level/types.go index ac95324..7626d1e 100644 --- a/pkg/level/types.go +++ b/pkg/level/types.go @@ -3,6 +3,7 @@ package level import ( "encoding/json" "fmt" + "os" "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/drawtool" @@ -31,8 +32,8 @@ type Base struct { // Level is the container format for Doodle map drawings. type Level struct { Base - Password string `json:"passwd"` - Difficulty enum.Difficulty `json:"difficulty"` + Password string `json:"passwd"` + GameRule GameRule `json:"rules"` // Chunked pixel data. Chunker *Chunker `json:"chunks"` @@ -58,11 +59,19 @@ type Level struct { UndoHistory *drawtool.History `json:"-"` } +// GameRule +type GameRule struct { + Difficulty enum.Difficulty `json:"difficulty"` + Survival bool `json:"survival"` +} + // New creates a blank level object with all its members initialized. func New() *Level { return &Level{ Base: Base{ Version: 1, + Title: "Untitled", + Author: os.Getenv("USER"), }, Chunker: NewChunker(balance.ChunkSize), Palette: &Palette{}, diff --git a/pkg/play_scene.go b/pkg/play_scene.go index d4b07ec..818b1b0 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -576,7 +576,7 @@ func (s *PlayScene) ShowEndLevelModal(success bool, title, message string) { log.Info("Mark level '%s' from pack '%s' as completed", s.Filename, s.LevelPack.Filename) if !s.cheated { elapsed := time.Since(s.startTime) - highscore := save.NewHighScore(s.LevelPack.Filename, s.Filename, s.perfectRun, elapsed) + highscore := save.NewHighScore(s.LevelPack.Filename, s.Filename, s.perfectRun, elapsed, s.Level.GameRule) if highscore { s.d.Flash("New record!") config.NewRecord = true diff --git a/pkg/savegame/savegame.go b/pkg/savegame/savegame.go index e46903d..b0f6499 100644 --- a/pkg/savegame/savegame.go +++ b/pkg/savegame/savegame.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/usercfg" "git.kirsle.net/apps/doodle/pkg/userdir" ) @@ -130,7 +131,7 @@ func (sg *SaveGame) MarkCompleted(levelpack, filename string) { // than the stored one it will update. // // Returns true if a new high score was logged. -func (sg *SaveGame) NewHighScore(levelpack, filename string, isPerfect bool, elapsed time.Duration) bool { +func (sg *SaveGame) NewHighScore(levelpack, filename string, isPerfect bool, elapsed time.Duration, rules level.GameRule) bool { levelpack = filepath.Base(levelpack) filename = filepath.Base(filename) @@ -144,9 +145,19 @@ func (sg *SaveGame) NewHighScore(levelpack, filename string, isPerfect bool, ela newHigh = true } } else { - if score.BestTime == nil || *score.BestTime > elapsed { - score.BestTime = &elapsed - newHigh = true + // GameRule: Survival (silver) - high score is based on longest time left alive rather + // than fastest time completed. + if rules.Survival { + if score.BestTime == nil || *score.BestTime < elapsed { + score.BestTime = &elapsed + newHigh = true + } + } else { + // Normally: fastest time is best time. + if score.BestTime == nil || *score.BestTime > elapsed { + score.BestTime = &elapsed + newHigh = true + } } } diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index 9215894..cb7d006 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -5,6 +5,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/go/render" "git.kirsle.net/go/render/event" @@ -107,14 +108,8 @@ func (w *Canvas) commitStroke(tool drawtool.Tool, addHistory bool) { } // Add the stroke to level history. - if w.level != nil && addHistory { - w.level.UndoHistory.AddStroke(w.currentStroke) - } else if w.doodad != nil && addHistory { - if w.doodad.UndoHistory == nil { - // HACK: if UndoHistory was not initialized properly. - w.doodad.UndoHistory = drawtool.NewHistory(balance.UndoHistory) - } - w.doodad.UndoHistory.AddStroke(w.currentStroke) + if addHistory { + w.strokeToHistory(w.currentStroke) } w.RemoveStroke(w.currentStroke) @@ -123,6 +118,19 @@ func (w *Canvas) commitStroke(tool drawtool.Tool, addHistory bool) { w.lastPixel = nil } +// Add a recently drawn stroke to the UndoHistory. +func (w *Canvas) strokeToHistory(stroke *drawtool.Stroke) { + if w.level != nil { + w.level.UndoHistory.AddStroke(stroke) + } else if w.doodad != nil { + if w.doodad.UndoHistory == nil { + // HACK: if UndoHistory was not initialized properly. + w.doodad.UndoHistory = drawtool.NewHistory(balance.UndoHistory) + } + w.doodad.UndoHistory.AddStroke(stroke) + } +} + // loopEditable handles the Loop() part for editable canvases. func (w *Canvas) loopEditable(ev *event.State) error { // Get the absolute position of the canvas on screen to accurately match @@ -146,7 +154,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { switch w.Tool { case drawtool.PanTool: // Pan tool = click to pan the level. - if ev.Button1 || keybind.MiddleClick(ev) { + if keybind.LeftClick(ev) || keybind.MiddleClick(ev) { if !w.scrollDragging { w.scrollDragging = true w.scrollStartAt = shmem.Cursor @@ -175,7 +183,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { } // Clicking? Log all the pixels while doing so. - if ev.Button1 { + if keybind.LeftClick(ev) { // Initialize a new Stroke for this atomic drawing operation? if w.currentStroke == nil { w.currentStroke = drawtool.NewStroke(drawtool.Freehand, w.Palette.ActiveSwatch.Color) @@ -221,6 +229,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { } else { w.commitStroke(w.Tool, true) } + case drawtool.LineTool: // If no swatch is active, do nothing with mouse clicks. if w.Palette.ActiveSwatch == nil { @@ -228,7 +237,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { } // Clicking? Log all the pixels while doing so. - if ev.Button1 { + if keybind.LeftClick(ev) { // Initialize a new Stroke for this atomic drawing operation? if w.currentStroke == nil { w.currentStroke = drawtool.NewStroke(drawtool.Line, w.Palette.ActiveSwatch.Color) @@ -243,6 +252,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { } else { w.commitStroke(w.Tool, true) } + case drawtool.RectTool: // If no swatch is active, do nothing with mouse clicks. if w.Palette.ActiveSwatch == nil { @@ -250,7 +260,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { } // Clicking? Log all the pixels while doing so. - if ev.Button1 { + if keybind.LeftClick(ev) { // Initialize a new Stroke for this atomic drawing operation? if w.currentStroke == nil { w.currentStroke = drawtool.NewStroke(drawtool.Rectangle, w.Palette.ActiveSwatch.Color) @@ -265,12 +275,13 @@ func (w *Canvas) loopEditable(ev *event.State) error { } else { w.commitStroke(w.Tool, true) } + case drawtool.EllipseTool: if w.Palette.ActiveSwatch == nil { return nil } - if ev.Button1 { + if keybind.LeftClick(ev) { if w.currentStroke == nil { w.currentStroke = drawtool.NewStroke(drawtool.Ellipse, w.Palette.ActiveSwatch.Color) w.currentStroke.Pattern = w.Palette.ActiveSwatch.Pattern @@ -284,6 +295,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { } else { w.commitStroke(w.Tool, true) } + case drawtool.TextTool: // The Text Tool popup should initialize this for us, if somehow not // initialized skip this tool processing. @@ -312,7 +324,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { // at the cursor location while the TextTool is active. // On mouse click, commit the text to the drawing. - if ev.Button1 { + if keybind.LeftClick(ev) { if stroke, err := drawtool.TT.ToStroke(shmem.CurrentRenderEngine, w.Palette.ActiveSwatch.Color, cursor); err != nil { shmem.FlashError("Text Tool error: %s", err) return nil @@ -322,12 +334,83 @@ func (w *Canvas) loopEditable(ev *event.State) error { w.commitStroke(drawtool.PencilTool, true) } - ev.Button1 = false + keybind.ClearLeftClick(ev) + } + + case drawtool.FloodTool: + if w.Palette.ActiveSwatch == nil { + return nil + } + + // Click to activate. + if keybind.LeftClick(ev) { + var ( + chunker = w.Chunker() + stroke = drawtool.NewStroke(drawtool.Freehand, w.Palette.ActiveSwatch.Color) + ) + + // Set some max boundaries to prevent runaway infinite loops, e.g. if user + // clicked the wide open void the flood fill would never finish! + limit := balance.FloodToolLimit + + // Get the original color at this location. + // Error cases can include: no chunk at this spot, or no pixel at this spot. + // Treat these as just a null color and proceed anyway, user should be able + // to flood fill blank areas of their level. + baseColor, err := chunker.Get(cursor) + if err != nil { + limit = balance.FloodToolVoidLimit + log.Warn("FloodTool: couldn't get base color at %s: %s (got %s)", cursor, err) + } + + // If no change, do nothing. + if baseColor == w.Palette.ActiveSwatch { + break + } + + // The flood fill algorithm. + queue := []render.Point{cursor} + for len(queue) > 0 { + node := queue[0] + queue = queue[1:] + + colorAt, _ := chunker.Get(node) + if colorAt != baseColor { + continue + } + + // For Undo history, store the original color at this point. + if colorAt != nil { + stroke.OriginalPoints[node] = colorAt + } + + // Add the neighboring pixels. + for _, neighbor := range []render.Point{ + {X: node.X - 1, Y: node.Y}, + {X: node.X + 1, Y: node.Y}, + {X: node.X, Y: node.Y - 1}, + {X: node.X, Y: node.Y + 1}, + } { + // Only if not too far from the origin! + if render.AbsInt(neighbor.X-cursor.X) <= limit && render.AbsInt(neighbor.Y-cursor.Y) <= limit { + queue = append(queue, neighbor) + } + } + + stroke.AddPoint(node) + err = chunker.Set(node, w.Palette.ActiveSwatch) + if err != nil { + log.Error("FloodTool: error setting %s to %s: %s", node, w.Palette.ActiveSwatch, err) + } + } + + w.strokeToHistory(stroke) + keybind.ClearLeftClick(ev) } case drawtool.EraserTool: // Clicking? Log all the pixels while doing so. - if ev.Button1 { + if keybind.LeftClick(ev) { // Initialize a new Stroke for this atomic drawing operation? if w.currentStroke == nil { // The color is white, will look like white-out that covers the @@ -370,6 +453,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { } else { w.commitStroke(w.Tool, true) } + case drawtool.ActorTool: // See if any of the actors are below the mouse cursor. var WP = w.WorldIndexAt(cursor) @@ -406,7 +490,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { // Check for a mouse down event to begin dragging this // canvas around. - if ev.Button1 { + if keybind.LeftClick(ev) { // Pop this canvas out for the drag/drop. if w.OnDragStart != nil { deleteActors = append(deleteActors, actor.Actor) @@ -427,6 +511,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { if len(deleteActors) > 0 && w.OnDeleteActors != nil { w.OnDeleteActors(deleteActors) } + case drawtool.LinkTool: // See if any of the actors are below the mouse cursor. var WP = w.WorldIndexAt(cursor) @@ -467,7 +552,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { }) // Click handler to start linking this actor. - if ev.Button1 { + if keybind.LeftClick(ev) { if err := w.LinkAdd(actor); err != nil { return err } @@ -475,7 +560,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { // TODO: reset the Button1 state so we don't finish a // link and then LinkAdd the clicked doodad immediately // (causing link chaining) - ev.Button1 = false + keybind.ClearLeftClick(ev) break } } else { diff --git a/pkg/uix/scripting.go b/pkg/uix/scripting.go index 3223e43..911faab 100644 --- a/pkg/uix/scripting.go +++ b/pkg/uix/scripting.go @@ -66,7 +66,7 @@ func (w *Canvas) MakeScriptAPI(vm *scripting.VM) { }) vm.Set("Level", map[string]interface{}{ - "Difficulty": w.level.Difficulty, + "Difficulty": w.level.GameRule.Difficulty, }) } diff --git a/pkg/windows/add_edit_level.go b/pkg/windows/add_edit_level.go index 9317f78..2f34954 100644 --- a/pkg/windows/add_edit_level.go +++ b/pkg/windows/add_edit_level.go @@ -1,6 +1,8 @@ package windows import ( + "fmt" + "regexp" "strconv" "git.kirsle.net/apps/doodle/pkg/balance" @@ -48,15 +50,12 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { window.SetButtons(ui.CloseButton) window.Configure(ui.Config{ Width: 400, - Height: 280, + Height: 290, Background: render.Grey, }) // Tabbed UI for New Level or New Doodad. tabframe := ui.NewTabFrame("Level Tabs") - if config.EditLevel != nil { - tabframe.SetTabsHidden(true) - } window.Pack(tabframe, ui.Pack{ Side: ui.N, Fill: true, @@ -64,8 +63,14 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { }) // Add the tabs. - config.setupLevelFrame(tabframe) - config.setupDoodadFrame(tabframe) + config.setupLevelFrame(tabframe) // Level Properties (always) + if config.EditLevel == nil { + // New Doodad properties (New window only) + config.setupDoodadFrame(tabframe) + } else { + // Additional Level tabs (existing level only) + config.setupGameRuleFrame(tabframe) + } tabframe.Supervise(config.Supervisor) @@ -77,6 +82,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { func (config AddEditLevel) setupLevelFrame(tf *ui.TabFrame) { // Default options. var ( + tabLabel = "New Level" newPageType = level.Bounded.String() newWallpaper = "notebook.png" paletteName = level.DefaultPaletteNames[0] @@ -94,13 +100,14 @@ func (config AddEditLevel) setupLevelFrame(tf *ui.TabFrame) { // Given a level to edit? if !isNewLevel { + tabLabel = "Properties" newPageType = config.EditLevel.PageType.String() newWallpaper = config.EditLevel.Wallpaper paletteName = textCurrentPalette } frame := tf.AddTab("index", ui.NewLabel(ui.Label{ - Text: "New Level", + Text: tabLabel, Font: balance.TabFont, })) @@ -126,10 +133,6 @@ func (config AddEditLevel) setupLevelFrame(tf *ui.TabFrame) { Label: "Page type:", Font: balance.UIFont, Options: []magicform.Option{ - { - Label: "Bounded", - Value: level.Bounded, - }, { Label: "Bounded", Value: level.Bounded, @@ -286,35 +289,33 @@ func (config AddEditLevel) setupLevelFrame(tf *ui.TabFrame) { ******************/ if config.EditLevel != nil { + var ( + levelSizeStr = fmt.Sprintf("%dx%d", config.EditLevel.MaxWidth, config.EditLevel.MaxHeight) + levelSizeRegexp = regexp.MustCompile(`^(\d+)x(\d+)$`) + ) fields = append(fields, []magicform.Field{ { - Label: "Difficulty:", - Font: balance.UIFont, - SelectValue: config.EditLevel.Difficulty, - Tooltip: ui.Tooltip{ - Text: "Peaceful: enemies may not attack\n" + - "Normal: default difficulty\n" + - "Hard: enemies may be more aggressive", - Edge: ui.Top, - }, - Options: []magicform.Option{ - { - Label: "Peaceful", - Value: enum.Peaceful, - }, - { - Label: "Normal (recommended)", - Value: enum.Normal, - }, - { - Label: "Hard", - Value: enum.Hard, - }, - }, - OnSelect: func(v interface{}) { - value, _ := v.(enum.Difficulty) - config.EditLevel.Difficulty = value - log.Info("Set level difficulty to: %d (%s)", value, value) + Label: "Limits (bounded):", + Font: balance.UIFont, + TextVariable: &levelSizeStr, + OnClick: func() { + shmem.Prompt(fmt.Sprintf("Enter new limits in WxH format or [%s]: ", levelSizeStr), func(answer string) { + if answer == "" { + return + } + + match := levelSizeRegexp.FindStringSubmatch(answer) + if match == nil { + return + } + + levelSizeStr = match[0] + width, _ := strconv.Atoi(match[1]) + height, _ := strconv.Atoi(match[2]) + + config.EditLevel.MaxWidth = int64(width) + config.EditLevel.MaxHeight = int64(height) + }) }, }, { @@ -341,7 +342,7 @@ func (config AddEditLevel) setupLevelFrame(tf *ui.TabFrame) { } // The confirm/cancel buttons. - var okLabel = "Ok" + var okLabel = "Apply" if config.EditLevel == nil { okLabel = "Continue" } @@ -537,3 +538,70 @@ func (config AddEditLevel) setupDoodadFrame(tf *ui.TabFrame) { }) } } + +// Creates the Game Rules frame for existing level (set difficulty, etc.) +func (config AddEditLevel) setupGameRuleFrame(tf *ui.TabFrame) { + frame := tf.AddTab("GameRules", ui.NewLabel(ui.Label{ + Text: "Game Rules", + Font: balance.TabFont, + })) + + form := magicform.Form{ + Supervisor: config.Supervisor, + Engine: config.Engine, + Vertical: true, + LabelWidth: 120, + PadY: 2, + } + fields := []magicform.Field{ + { + Label: "Game Rules are specific to this level and can change some of\n" + + "the game's default behaviors.", + Font: balance.UIFont, + }, + { + Label: "Difficulty:", + Font: balance.UIFont, + SelectValue: config.EditLevel.GameRule.Difficulty, + Tooltip: ui.Tooltip{ + Text: "Peaceful: enemies may not attack\n" + + "Normal: default difficulty\n" + + "Hard: enemies may be more aggressive", + Edge: ui.Top, + }, + Options: []magicform.Option{ + { + Label: "Peaceful", + Value: enum.Peaceful, + }, + { + Label: "Normal (recommended)", + Value: enum.Normal, + }, + { + Label: "Hard", + Value: enum.Hard, + }, + }, + OnSelect: func(v interface{}) { + value, _ := v.(enum.Difficulty) + config.EditLevel.GameRule.Difficulty = value + log.Info("Set level difficulty to: %d (%s)", value, value) + }, + }, + { + Label: "Survival Mode (silver high score)", + Font: balance.UIFont, + BoolVariable: &config.EditLevel.GameRule.Survival, + Tooltip: ui.Tooltip{ + Text: "Use for levels where dying at least once is very likely\n" + + "(e.g. Azulian Tag). The silver high score will be for\n" + + "longest time rather than fastest time. The gold high\n" + + "score will still be for fastest time.", + Edge: ui.Top, + }, + }, + } + + form.Create(frame, fields) +} diff --git a/pkg/windows/levelpack_open.go b/pkg/windows/levelpack_open.go index 9f82a52..2fcd5a4 100644 --- a/pkg/windows/levelpack_open.go +++ b/pkg/windows/levelpack_open.go @@ -466,7 +466,7 @@ func (config LevelPack) makeDetailScreen(frame *ui.Frame, width, height int, lp btn := ui.NewButton(level.Filename, btnFrame) btn.Handle(ui.Click, func(ed ui.EventData) error { // Is this level locked? - if locked { + if locked && !balance.CheatEnabledUnlockLevels { modal.Alert( "This level hasn't been unlocked! Complete the earlier\n" + "levels in this pack to unlock later levels.", diff --git a/pkg/windows/palette_editor.go b/pkg/windows/palette_editor.go index de64137..57fb1b5 100644 --- a/pkg/windows/palette_editor.go +++ b/pkg/windows/palette_editor.go @@ -7,6 +7,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/apps/doodle/pkg/modal" "git.kirsle.net/apps/doodle/pkg/pattern" "git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/usercfg" @@ -118,6 +119,8 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window { // Draw the main table of Palette rows. if pal := config.EditPalette; pal != nil { for i, swatch := range pal.Swatches { + i := i + var idStr = fmt.Sprintf("%d", i) swatch := swatch @@ -153,6 +156,14 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window { shmem.Prompt("New swatch name ["+swatch.Name+"]: ", func(answer string) { log.Warn("Answer: %s", answer) if answer != "" { + // Confirm it is unique. + for j, exist := range pal.Swatches { + if exist.Name == answer && i != j { + modal.Alert("That name is already used by another color.") + return + } + } + swatch.Name = answer if config.OnChange != nil { config.OnChange() @@ -166,19 +177,8 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window { ////////////// // Color Choice button. btnColor := ui.NewButton("Color", ui.NewFrame("Color Frame")) - btnColor.SetStyle(&style.Button{ - Background: swatch.Color, - HoverBackground: swatch.Color.Lighten(40), - OutlineColor: render.Black, - OutlineSize: 1, - BorderStyle: style.BorderRaised, - BorderSize: 2, - }) - btnColor.Configure(ui.Config{ - Background: swatch.Color, - Width: col2, - Height: 24, - }) + setPaletteButtonColor(btnColor, swatch.Color) + btnColor.Resize(render.NewRect(col2, 24)) btnColor.Handle(ui.Click, func(ed ui.EventData) error { // Open a ColorPicker widget. picker, err := ui.NewColorPicker(ui.ColorPicker{ @@ -214,17 +214,7 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window { picker.Then(func(color render.Color) { swatch.Color = color - - // TODO: redundant from above, consolidate these - fmt.Printf("Set button style to: %s\n", swatch.Color) - btnColor.SetStyle(&style.Button{ - Background: swatch.Color, - HoverBackground: swatch.Color.Lighten(40), - OutlineColor: render.Black, - OutlineSize: 1, - BorderStyle: style.BorderRaised, - BorderSize: 2, - }) + setPaletteButtonColor(btnColor, color) if config.OnChange != nil { config.OnChange() @@ -243,10 +233,7 @@ func NewPaletteEditor(config PaletteEditor) *ui.Window { selTexture := ui.NewSelectBox("Texture", ui.Label{ Font: balance.MenuFont, }) - selTexture.Configure(ui.Config{ - Width: col5, - Height: 24, - }) + selTexture.Resize(render.NewRect(col5, 24)) for _, t := range pattern.Builtins { if t.Hidden && !usercfg.Current.ShowHiddenDoodads { @@ -459,3 +446,15 @@ func setImageOnSelect(sel *ui.SelectBox, filename string) { sel.SetImage(nil) } } + +// Helper function to assign a palette "color button" color. +func setPaletteButtonColor(btn *ui.Button, color render.Color) { + btn.SetStyle(&style.Button{ + Background: color, + HoverBackground: color.Lighten(40), + OutlineColor: render.Black, + OutlineSize: 1, + BorderStyle: style.BorderRaised, + BorderSize: 2, + }) +}