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.
This commit is contained in:
parent
bf706efdc6
commit
af6b8625d6
BIN
assets/sprites/flood-tool.png
Normal file
BIN
assets/sprites/flood-tool.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -38,8 +38,8 @@ const (
|
|||
|
||||
func (d Difficulty) String() string {
|
||||
return []string{
|
||||
"Peaceful",
|
||||
"Normal",
|
||||
"Hard",
|
||||
"Peaceful",
|
||||
}[d]
|
||||
}[d+1]
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
@ -32,7 +33,7 @@ type Base struct {
|
|||
type Level struct {
|
||||
Base
|
||||
Password string `json:"passwd"`
|
||||
Difficulty enum.Difficulty `json:"difficulty"`
|
||||
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{},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,11 +145,21 @@ func (sg *SaveGame) NewHighScore(levelpack, filename string, isPerfect bool, ela
|
|||
newHigh = true
|
||||
}
|
||||
} else {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if newHigh {
|
||||
if sg.LevelPacks[levelpack] == nil {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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.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:",
|
||||
Label: "Limits (bounded):",
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user