Flood Tool, Survival Mode for Azulian Tag

New features:
* Flood Tool for the editor. It replaces pixels of one color with another,
  contiguously. Has limits on how far from the original pixel it will color,
  to avoid infinite loops in case the user clicked on wide open void. The
  limit when clicking an existing color is 1200px or only a 600px limit if
  clicking into the void.
* Cheat code: 'master key' to play locked Story Mode levels.

Level GameRules feature added:
* A new tab in the Level Properties dialog
* Difficulty has been moved to this tab
* Survival Mode: for silver high score, longest time alive is better than
  fastest time, for Azulian Tag maps. Gold high score is still based on
  fastest time - find the hidden level exit without dying!

Tweaks to the Azulians' jump heights:
* Blue Azulian:  12 -> 14
* Red Azulian:   14 -> 18
* White Azulian: 16 -> 20

Bugs fixed:
* When editing your Palette to rename a color or add a new color, it wasn't
  possible to draw with that color until the editor was completely unloaded
  and reloaded; this is now fixed.
* Minor bugfix in Difficulty.String() for Peaceful (-1) difficulty to avoid
  a negative array index.
* Try and prevent user giving the same name to multiple swatches on their
  palette. Replacing the whole palette can let duplication through still.
pull/84/head
Noah 2022-03-26 13:55:06 -07:00
parent bf706efdc6
commit af6b8625d6
22 changed files with 354 additions and 125 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -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) {

View File

@ -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
)

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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{

View File

@ -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)

View File

@ -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.

View File

@ -38,8 +38,8 @@ const (
func (d Difficulty) String() string {
return []string{
"Peaceful",
"Normal",
"Hard",
"Peaceful",
}[d]
}[d+1]
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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()

View File

@ -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{},

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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 {

View File

@ -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,
})
}

View File

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

View File

@ -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.",

View File

@ -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,
})
}