doodle/pkg/windows/add_edit_level.go

705 lines
18 KiB
Go
Raw Permalink Normal View History

package windows
import (
"fmt"
"regexp"
"strconv"
2022-09-24 22:17:25 +00:00
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
"git.kirsle.net/SketchyMaze/doodle/pkg/enum"
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/doodle/pkg/modal"
"git.kirsle.net/SketchyMaze/doodle/pkg/native"
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
magicform "git.kirsle.net/SketchyMaze/doodle/pkg/uix/magic-form"
"git.kirsle.net/SketchyMaze/doodle/pkg/wallpaper"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
Update savegame format, Allow out-of-bounds camera Updates the savegame.json file format: * Levels now have a UUID value assigned at first save. * The savegame.json will now track level completion/score based on UUID, making it robust to filename changes in either levels or levelpacks. * The savegame file is auto-migrated on startup - for any levels not found or have no UUID, no change is made, it's backwards compatible. * Level Properties window adds an "Advanced" tab to show/re-roll UUID. New JavaScript API for doodad scripts: * `Actors.CameraFollowPlayer()` tells the camera to return focus to the player character. Useful for "cutscene" doodads that freeze the player, call `Self.CameraFollowMe()` and do a thing before unfreezing and sending the camera back to the player. (Or it will follow them at their next directional input control). * `Self.MoveBy(Point(x, y int))` to move the current actor a bit. New option for the `doodad` command-line tool: * `doodad resave <.level or .doodad>` will load and re-save a drawing, to migrate it to the newest file format versions. Small tweaks: * On bounded levels, allow the camera to still follow the player if the player finds themselves WELL far out of bounds (40 pixels margin). So on bounded levels you can create "interior rooms" out-of-bounds to Warp Door into. * New wallpaper: "Atmosphere" has a black starscape pattern that fades into a solid blue atmosphere. * Camera strictly follows the player the first 20 ticks, not 60 of level start * If player is frozen, directional inputs do not take the camera focus back.
2023-03-08 05:55:10 +00:00
"github.com/google/uuid"
)
// AddEditLevel is the "Create New Level & Edit Level Properties" window
type AddEditLevel struct {
Supervisor *ui.Supervisor
Engine render.Engine
// Editing settings for an existing level?
EditLevel *level.Level
// Show the "New Doodad" tab by default?
NewDoodad bool
// Callback functions.
OnChangePageTypeAndWallpaper func(pageType level.PageType, wallpaper string)
OnCreateNewLevel func(*level.Level)
OnCreateNewDoodad func(width, height int)
OnUpdateScreenshot func() error
OnReload func()
OnCancel func()
}
// NewAddEditLevel initializes the window.
func NewAddEditLevel(config AddEditLevel) *ui.Window {
// Default options.
var (
title = "New Drawing"
)
// Given a level to edit?
if config.EditLevel != nil {
title = "Level Properties"
}
window := ui.NewWindow(title)
window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{
Width: 400,
Height: 290,
Background: render.Grey,
})
// Tabbed UI for New Level or New Doodad.
tabframe := ui.NewTabFrame("Level Tabs")
window.Pack(tabframe, ui.Pack{
Side: ui.N,
Fill: true,
Expand: true,
})
// Add the tabs.
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)
config.setupScreenshotFrame(tabframe)
Update savegame format, Allow out-of-bounds camera Updates the savegame.json file format: * Levels now have a UUID value assigned at first save. * The savegame.json will now track level completion/score based on UUID, making it robust to filename changes in either levels or levelpacks. * The savegame file is auto-migrated on startup - for any levels not found or have no UUID, no change is made, it's backwards compatible. * Level Properties window adds an "Advanced" tab to show/re-roll UUID. New JavaScript API for doodad scripts: * `Actors.CameraFollowPlayer()` tells the camera to return focus to the player character. Useful for "cutscene" doodads that freeze the player, call `Self.CameraFollowMe()` and do a thing before unfreezing and sending the camera back to the player. (Or it will follow them at their next directional input control). * `Self.MoveBy(Point(x, y int))` to move the current actor a bit. New option for the `doodad` command-line tool: * `doodad resave <.level or .doodad>` will load and re-save a drawing, to migrate it to the newest file format versions. Small tweaks: * On bounded levels, allow the camera to still follow the player if the player finds themselves WELL far out of bounds (40 pixels margin). So on bounded levels you can create "interior rooms" out-of-bounds to Warp Door into. * New wallpaper: "Atmosphere" has a black starscape pattern that fades into a solid blue atmosphere. * Camera strictly follows the player the first 20 ticks, not 60 of level start * If player is frozen, directional inputs do not take the camera focus back.
2023-03-08 05:55:10 +00:00
config.setupAdvancedFrame(tabframe)
}
tabframe.Supervise(config.Supervisor)
// Show the doodad tab?
if config.NewDoodad {
tabframe.SetTab("doodad")
}
window.Hide()
return window
}
// Creates the Create/Edit Level tab ("index").
func (config AddEditLevel) setupLevelFrame(tf *ui.TabFrame) {
// Default options.
var (
tabLabel = "New Level"
newPageType = level.Bounded.String()
newWallpaper = "notebook.png"
paletteName = level.DefaultPaletteNames[0]
isNewLevel = config.EditLevel == nil
// Default text for the Palette drop-down for already-existing levels.
// (needs --experimental feature flag to enable the UI).
textCurrentPalette = "Keep current palette"
// For NEW levels, if a custom wallpaper is selected from disk, cache
// it in these vars. For pre-existing levels, the wallpaper updates
// immediately in the live config.EditLevel object.
newWallpaperB64 string
)
// 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: tabLabel,
Font: balance.TabFont,
}))
/******************
* Frame for selecting Page Type
******************/
// Selected "Page Type" property.
var pageType = level.Bounded
if !isNewLevel {
pageType = config.EditLevel.PageType
}
form := magicform.Form{
Supervisor: config.Supervisor,
Engine: config.Engine,
Vertical: true,
LabelWidth: 120,
PadY: 2,
}
fields := []magicform.Field{
{
Label: "Page type:",
Font: balance.UIFont,
Options: []magicform.Option{
{
Label: "Bounded",
Value: level.Bounded,
},
{
Label: "Unbounded",
Value: level.Unbounded,
},
{
Label: "No Negative Space",
Value: level.NoNegativeSpace,
},
},
SelectValue: pageType,
OnSelect: func(v interface{}) {
value, _ := v.(level.PageType)
newPageType = value.String() // for the "New" screen background
config.OnChangePageTypeAndWallpaper(value, newWallpaper)
},
},
}
/******************
* Wallpaper settings
******************/
var selectedWallpaper = "notebook.png"
if config.EditLevel != nil {
selectedWallpaper = config.EditLevel.Wallpaper
}
fields = append(fields, []magicform.Field{
{
Label: "Wallpaper:",
Font: balance.UIFont,
SelectValue: selectedWallpaper,
Options: balance.Wallpapers,
OnSelect: func(v interface{}) {
if filename, ok := v.(string); ok {
// Picking the Custom option?
if filename == balance.CustomWallpaperFilename {
filename, err := native.OpenFile("Choose a custom wallpaper:", "*.png *.jpg *.gif")
if err == nil {
b64data, err := wallpaper.FileToB64(filename)
if err != nil {
shmem.Flash("Error loading wallpaper: %s", err)
return
}
// If editing a level, apply the update straight away.
if config.EditLevel != nil {
config.EditLevel.SetFile(balance.CustomWallpaperEmbedPath, []byte(b64data))
newWallpaper = balance.CustomWallpaperFilename
// Trigger the page type change to the caller.
if pageType, ok := level.PageTypeFromString(newPageType); ok {
config.OnChangePageTypeAndWallpaper(pageType, balance.CustomWallpaperFilename)
}
} else {
// Hold onto the new wallpaper until the level is created.
newWallpaper = balance.CustomWallpaperFilename
newWallpaperB64 = b64data
}
}
return
}
if pageType, ok := level.PageTypeFromString(newPageType); ok {
config.OnChangePageTypeAndWallpaper(pageType, filename)
newWallpaper = filename
}
}
},
},
}...)
/******************
* Frame for picking a default color palette.
******************/
// For new level or --experimental only.
if config.EditLevel == nil || balance.Feature.ChangePalette {
var (
palettes = []magicform.Option{}
)
if config.EditLevel != nil {
palettes = append(palettes, []magicform.Option{
{
Label: paletteName, // "Keep current palette"
Value: paletteName,
},
{
Separator: true,
},
}...)
}
for _, palName := range level.DefaultPaletteNames {
palettes = append(palettes, magicform.Option{
Label: palName,
Value: palName,
})
}
// Add form fields.
fields = append(fields, []magicform.Field{
{
Label: "Palette:",
Font: balance.UIFont,
Options: palettes,
OnSelect: func(v interface{}) {
value, _ := v.(string)
paletteName = value
},
},
}...)
}
/******************
* Extended options for editing existing level (vs. Create New screen)
******************/
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: "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)
})
},
},
{
Label: "Metadata",
Font: balance.LabelFont,
},
{
Label: "Title:",
Font: balance.UIFont,
TextVariable: &config.EditLevel.Title,
PromptUser: func(answer string) {
config.EditLevel.Title = answer
},
},
{
Label: "Author:",
Font: balance.UIFont,
TextVariable: &config.EditLevel.Author,
PromptUser: func(answer string) {
config.EditLevel.Author = answer
},
},
}...)
}
// The confirm/cancel buttons.
var okLabel = "Apply"
if config.EditLevel == nil {
okLabel = "Continue"
}
fields = append(fields, []magicform.Field{
{
Buttons: []magicform.Field{
{
ButtonStyle: &balance.ButtonPrimary,
Label: okLabel,
Font: balance.UIFont,
OnClick: func() {
// Is it a NEW level?
if config.EditLevel == nil {
shmem.Flash("Create new map with %s page type and %s wallpaper", newPageType, newWallpaper)
pageType, ok := level.PageTypeFromString(newPageType)
if !ok {
shmem.Flash("Invalid Page Type '%s'", newPageType)
return
}
lvl := level.New()
lvl.Palette = level.DefaultPalettes[paletteName]
lvl.Wallpaper = newWallpaper
lvl.PageType = pageType
// Was a custom wallpaper selected for our NEW level?
if lvl.Wallpaper == balance.CustomWallpaperFilename && len(newWallpaperB64) > 0 {
lvl.SetFile(balance.CustomWallpaperEmbedPath, []byte(newWallpaperB64))
}
if config.OnCreateNewLevel != nil {
config.OnCreateNewLevel(lvl)
} else {
shmem.FlashError("OnCreateNewLevel not attached")
}
} else {
// Editing an existing level.
// If we're editing a level, did we select a new palette?
// Warn the user about if they want to change palettes.
if paletteName != textCurrentPalette {
modal.Confirm(
"Are you sure you want to change the level palette?\n" +
"Existing pixels drawn on your level may change, and\n" +
"if the new palette is smaller, some pixels may be\n" +
"lost from your level. OK to continue?",
).WithTitle("Change Level Palette").Then(func() {
// Install the new level palette.
config.EditLevel.ReplacePalette(level.DefaultPalettes[paletteName])
if config.OnReload != nil {
config.OnReload()
}
})
return
}
config.OnCancel()
}
},
},
{
Label: "Cancel",
Font: balance.UIFont,
OnClick: func() {
config.OnCancel()
},
},
},
},
}...)
form.Create(frame, fields)
}
Various updates New doodad interactions: * Sticky Buttons will emit a "sticky:down" event to linked doodads, with a boolean value showing the Sticky Button's state. * Normal Buttons will listen for "sticky:down" -- when a linked Sticky Button is pressed, the normal Button presses in as well, and stays pressed while the sticky:down signal is true. * When the Sticky Button is released (e.g. because it received power from another doodad), any linked buttons which were sticky:down release as well. * Switch doodads emit a new "switch:toggle" event JUST BEFORE sending the "power" event. Sensitive Doodads can listen for switches in particular this way. * The Electric Door listens for switch:toggle; if a Switch is activated, the Electric Door always flips its current state (open to close, or vice versa) and ignores the immediately following power event. This allows doors to toggle on/off regardless of sync with a Switch. Other changes: * When the player character dies by fire, instead of the message saying "Watch out for fire!" it will use the name of the fire swatch that hurt the player. This way levels could make it say "Watch out for spikes!" or "lava" or whatever they want. The "Fire" attribute now just means "instantly kills the player." * Level Editor: You can now edit the Title and Author name of your level in the Page Settings window. * Bugfix: only the player character ends the game by dying in fire. Other mobile doodads just turn dark but don't end the game. * Increase the size of Trapdoor doodad sprites by 150% as they were a bit small for the player character. * Rename the game from "Project: Doodle" to "Sketchy Maze"
2021-03-31 06:33:25 +00:00
// Creates the "New Doodad" frame.
func (config AddEditLevel) setupDoodadFrame(tf *ui.TabFrame) {
// Default options.
var (
doodadWidth = 64
doodadHeight = doodadWidth
)
frame := tf.AddTab("doodad", ui.NewLabel(ui.Label{
Text: "New Doodad",
Font: balance.TabFont,
}))
/******************
* Frame for selecting Page Type
******************/
var sizeOptions = []magicform.Option{
{Label: "32", Value: 32},
{Label: "64", Value: 64},
{Label: "96", Value: 96},
{Label: "128", Value: 128},
{Label: "200", Value: 200},
{Label: "256", Value: 256},
{Label: "Custom...", Value: 0},
}
form := magicform.Form{
Supervisor: config.Supervisor,
Engine: config.Engine,
Vertical: true,
LabelWidth: 90,
}
form.Create(frame, []magicform.Field{
{
Label: "Width:",
Font: balance.LabelFont,
Type: magicform.Selectbox,
IntVariable: &doodadWidth,
Options: sizeOptions,
OnSelect: func(v interface{}) {
if v.(int) == 0 {
shmem.Prompt("Enter a custom size for the doodad width: ", func(answer string) {
if a, err := strconv.Atoi(answer); err == nil && a > 0 {
doodadWidth = a
} else {
shmem.FlashError("Doodad size should be a number greater than zero.")
}
})
}
},
},
{
Label: "Height:",
Font: balance.LabelFont,
Type: magicform.Selectbox,
IntVariable: &doodadHeight,
Options: sizeOptions,
OnSelect: func(v interface{}) {
if v.(int) == 0 {
shmem.Prompt("Enter a custom size for the doodad height: ", func(answer string) {
if a, err := strconv.Atoi(answer); err == nil && a > 0 {
doodadHeight = a
} else {
shmem.FlashError("Doodad size should be a number greater than zero.")
}
})
}
},
},
{
Buttons: []magicform.Field{
{
Label: "Continue",
Font: balance.UIFont,
ButtonStyle: &balance.ButtonPrimary,
OnClick: func() {
if config.OnCreateNewDoodad != nil {
config.OnCreateNewDoodad(doodadWidth, doodadHeight)
} else {
shmem.FlashError("OnCreateNewDoodad not attached")
}
},
},
{
Label: "Cancel",
Font: balance.UIFont,
ButtonStyle: &balance.ButtonPrimary,
OnClick: func() {
if config.OnCancel != nil {
config.OnCancel()
} else {
shmem.FlashError("OnCancel not attached")
}
},
},
},
},
})
}
// 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)
}
Update savegame format, Allow out-of-bounds camera Updates the savegame.json file format: * Levels now have a UUID value assigned at first save. * The savegame.json will now track level completion/score based on UUID, making it robust to filename changes in either levels or levelpacks. * The savegame file is auto-migrated on startup - for any levels not found or have no UUID, no change is made, it's backwards compatible. * Level Properties window adds an "Advanced" tab to show/re-roll UUID. New JavaScript API for doodad scripts: * `Actors.CameraFollowPlayer()` tells the camera to return focus to the player character. Useful for "cutscene" doodads that freeze the player, call `Self.CameraFollowMe()` and do a thing before unfreezing and sending the camera back to the player. (Or it will follow them at their next directional input control). * `Self.MoveBy(Point(x, y int))` to move the current actor a bit. New option for the `doodad` command-line tool: * `doodad resave <.level or .doodad>` will load and re-save a drawing, to migrate it to the newest file format versions. Small tweaks: * On bounded levels, allow the camera to still follow the player if the player finds themselves WELL far out of bounds (40 pixels margin). So on bounded levels you can create "interior rooms" out-of-bounds to Warp Door into. * New wallpaper: "Atmosphere" has a black starscape pattern that fades into a solid blue atmosphere. * Camera strictly follows the player the first 20 ticks, not 60 of level start * If player is frozen, directional inputs do not take the camera focus back.
2023-03-08 05:55:10 +00:00
// Level Screenshot management frame.
func (config AddEditLevel) setupScreenshotFrame(tf *ui.TabFrame) {
frame := tf.AddTab("Screenshot", ui.NewLabel(ui.Label{
Text: "Screenshot",
Font: balance.TabFont,
}))
var image *ui.Image
// Have a screenshot already?
if config.EditLevel.HasScreenshot() {
if img, err := config.EditLevel.GetScreenshotImageAsUIImage(balance.LevelScreenshotSmallFilename); err != nil {
lbl := ui.NewLabel(ui.Label{
Text: err.Error(),
Font: balance.DangerFont,
})
frame.Pack(lbl, ui.Pack{
Side: ui.N,
})
} else {
// Draw the image.
log.Error("Got img: %+v", img)
frame.Pack(img, ui.Pack{
Side: ui.N,
})
// Hold onto it in case we need to refresh it.
image = img
}
} else {
lbl := ui.NewLabel(ui.Label{
Text: "This level has no screenshot available. If this is\n" +
"a new drawing, its first screenshot will be created\n" +
"upon save; playtest or close and re-open the level\n" +
"then and its screenshot should appear here and can\n" +
"be refreshed.",
Font: balance.DangerFont,
})
frame.Pack(lbl, ui.Pack{
Side: ui.N,
})
// Exit now - don't add the Refresh button in case of nil pointer exception
// while we are in this state.
return
}
// Image refresh button.
btn := ui.NewButton("Refresh", ui.NewLabel(ui.Label{
Text: "Update Screenshot",
Font: balance.UIFont,
}))
ui.NewTooltip(btn, ui.Tooltip{
Text: "Update the screenshot from your current scroll point\nin the level editor.",
})
btn.Handle(ui.Click, func(ed ui.EventData) error {
// Take a new screenshot.
if config.OnUpdateScreenshot == nil {
shmem.FlashError("OnUpdateScreenshot handler not configured!")
} else {
if err := config.OnUpdateScreenshot(); err != nil {
modal.Alert(err.Error()).WithTitle("Error updating screenshot")
} else {
// Get the updated screenshot image from the level.
if img, err := config.EditLevel.GetScreenshotImage(balance.LevelScreenshotSmallFilename); err != nil {
shmem.FlashError("Couldn't reload screenshot from level: %s", err)
} else {
image.ReplaceFromImage(img)
}
}
}
return nil
})
btn.Compute(config.Engine)
frame.Pack(btn, ui.Pack{
Side: ui.N,
Expand: true,
})
config.Supervisor.Add(btn)
}
Update savegame format, Allow out-of-bounds camera Updates the savegame.json file format: * Levels now have a UUID value assigned at first save. * The savegame.json will now track level completion/score based on UUID, making it robust to filename changes in either levels or levelpacks. * The savegame file is auto-migrated on startup - for any levels not found or have no UUID, no change is made, it's backwards compatible. * Level Properties window adds an "Advanced" tab to show/re-roll UUID. New JavaScript API for doodad scripts: * `Actors.CameraFollowPlayer()` tells the camera to return focus to the player character. Useful for "cutscene" doodads that freeze the player, call `Self.CameraFollowMe()` and do a thing before unfreezing and sending the camera back to the player. (Or it will follow them at their next directional input control). * `Self.MoveBy(Point(x, y int))` to move the current actor a bit. New option for the `doodad` command-line tool: * `doodad resave <.level or .doodad>` will load and re-save a drawing, to migrate it to the newest file format versions. Small tweaks: * On bounded levels, allow the camera to still follow the player if the player finds themselves WELL far out of bounds (40 pixels margin). So on bounded levels you can create "interior rooms" out-of-bounds to Warp Door into. * New wallpaper: "Atmosphere" has a black starscape pattern that fades into a solid blue atmosphere. * Camera strictly follows the player the first 20 ticks, not 60 of level start * If player is frozen, directional inputs do not take the camera focus back.
2023-03-08 05:55:10 +00:00
// Creates the Game Rules frame for existing level (set difficulty, etc.)
func (config AddEditLevel) setupAdvancedFrame(tf *ui.TabFrame) {
frame := tf.AddTab("Advanced", ui.NewLabel(ui.Label{
Text: "Advanced",
Font: balance.TabFont,
}))
form := magicform.Form{
Supervisor: config.Supervisor,
Engine: config.Engine,
Vertical: true,
LabelWidth: 120,
PadY: 2,
}
fields := []magicform.Field{
{
Label: "Level UUID Number",
Font: balance.LabelFont,
},
{
Label: "Levels are assigned a unique identifier (UUID) for the purpose\n" +
"of saving your high scores for them (in levels which are part of\n" +
"level packs). Your level's UUID is shown below. Click it to\n" +
"re-roll a new UUID number (e.g. in case you save a new copy\n" +
"of a level that you want to be distinct from its original.)",
Font: balance.UIFont,
},
{
Label: "Level UUID:",
Font: balance.UIFont,
TextVariable: &config.EditLevel.UUID,
Tooltip: ui.Tooltip{
Text: "Click to re-roll a new UUID value.",
Edge: ui.Top,
},
OnClick: func() {
modal.Confirm(
"Are you sure you want to re-roll a new UUID value?\n\n" +
"Saving with a new UUID will mark this level as distinct\n" +
"from the previous version - if you place both versions\n" +
"in a levelpack together they will have separate high\n" +
"score values.",
).WithTitle("Re-roll UUID").Then(func() {
config.EditLevel.UUID = uuid.New().String()
})
},
},
}
form.Create(frame, fields)
}