package windows import ( "fmt" "regexp" "strconv" "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" "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) 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.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) } // 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) } // 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) }