Files
kiss-vscode/projects/flixel-desktop-habit-puzzle-game/source/HabitState.kiss

398 lines
18 KiB
Plaintext

(loadFrom "kiss-tools" "src/kiss_tools/RefactorUtil.kiss")
(prop &mut :Jigsawx jigsaw)
(prop &mut :FlxCamera pieceCamera)
(prop &mut :FlxCamera uiCamera)
(method &override :Void create []
(set pieceCamera FlxG.camera)
(set uiCamera (new FlxCamera))
(set uiCamera.bgColor FlxColor.TRANSPARENT)
(pieceCamera.copyFrom FlxG.camera)
(set FlxG.camera pieceCamera)
(FlxG.cameras.add uiCamera)
(FlxG.plugins.add (new FlxMouseControl))
(set FlxMouseControl.sortIndex "priorityID")
(set bgColor FlxColor.TRANSPARENT)
(super.create))
(var KEYBOARD_SCROLL_SPEED 200)
(prop &mut :EntryType typeAdding Todo)
(prop &mut :FlxInputText entryNameText)
(method &override :Void update [:Float elapsed]
(super.update elapsed)
(pieceCamera.updateScrollWheelZoom elapsed 1)
(pieceCamera.updateMouseBorderControl elapsed KEYBOARD_SCROLL_SPEED 0.15)
// Hold left-click to hide the habit text and see the image clearly:
(when entryTexts (if FlxG.mouse.pressed (remove entryTexts) (add entryTexts)))
(when FlxG.keys.justPressed.ESCAPE
(Sys.exit 0))
// TODO provide a saner/configurable set of bindings to trigger these ui action functions
{
(when (and FlxG.keys.justPressed.SPACE !entryNameText)
(defAndCall method toggleBackgroundColor
(set save.data.backgroundIndex #{(save.data.backgroundIndex + 1) % backgroundOptions.length;}#)
(save.flush)
// setModel so the entry text gets remade in inverted/lightened colors when necessary
(setModel model (nth model.rewardFiles rewardFileIndex))))
(method startAdding [:EntryType type]
(set typeAdding type)
(set entryNameText (new FlxInputText 0 0 FlxG.width "" textSize true))
(set entryNameText.cameras [uiCamera])
(set entryNameText.hasFocus true)
(add entryNameText))
(when FlxG.keys.justPressed.ENTER
(cond
(entryNameText
(model.addEntry typeAdding [entryNameText.text])
(setModel model (nth model.rewardFiles rewardFileIndex))
(entryNameText.kill)
(set entryNameText null))
(true
(startAdding Todo))))
(when FlxG.keys.justPressed.UP
(startAdding Bonus))
}
// drag along connected pieces
(when draggingSprite
(let [dx (- draggingSprite.x draggingLastPos.x)
dy (- draggingSprite.y draggingLastPos.y)]
(set draggingLastPos (new FlxPoint draggingSprite.x draggingSprite.y))
(doFor s (recursivelyConnectedPieces draggingSprite)
(+= s.x dx)
(+= s.y dy)))
// This hacks around a tricky edge case -- or so I thought
(when FlxG.mouse.justReleased
(draggingSprite.stopDrag)))
// Left and right arrow keys can switch between unlocked puzzles
(unless entryNameText
(when FlxG.keys.justPressed.LEFT
(unless (= rewardFileIndex 0)
(-= rewardFileIndex 1)
(setModel model (nth model.rewardFiles rewardFileIndex))))
(when FlxG.keys.justPressed.RIGHT
(unless (= rewardFileIndex maxRewardFile)
(+= rewardFileIndex 1)
(setModel model (nth model.rewardFiles rewardFileIndex)))))
// Handle keyboard input:
(when (and shortcutHandler !entryNameText)
(shortcutHandler.update)))
(prop &mut :FlxSave save null)
(prop &mut :FlxTypedGroup<FlxText> entryTexts null)
(prop &mut :FlxKeyShortcutHandler<Entry> shortcutHandler null)
(prop &mut :HabitModel model null)
(prop EDGE_LEEWAY 25)
(prop BUBBLE_SIZE 15)
(var PUZZLE_WIDTH 6)
(var PUZZLE_HEIGHT 5)
(var TOTAL_PIECES (* PUZZLE_WIDTH PUZZLE_HEIGHT))
(prop &mut :FlxTypedGroup<FlxExtendedSprite> rewardSprites null)
(prop &mut :Map<Int,FlxExtendedSprite> matchingPiecesLeft (new Map))
(prop &mut :Map<Int,FlxExtendedSprite> matchingPiecesRight (new Map))
(prop &mut :Map<Int,FlxExtendedSprite> matchingPiecesUp (new Map))
(prop &mut :Map<Int,FlxExtendedSprite> matchingPiecesDown (new Map))
(prop &mut :Map<Int,JigsawPiece> pieceData (new Map))
(prop &mut :Map<Int,Array<FlxExtendedSprite>> connectedPieces (new Map))
(prop &mut :Map<FlxExtendedSprite,Int> indexMap (new Map))
(prop &mut rewardFileIndex 0)
(prop &mut maxRewardFile 0)
(var SCROLL_BOUND_MARGIN 200)
(prop &mut :FlxExtendedSprite draggingSprite null)
(prop &mut :FlxPoint draggingLastPos null)
// Main.hx sets off 99% of the app's logic by parsing the model file and calling setModel on startup and on a 30s loop:
(method setModel [m &opt :RewardFile currentRewardFile]
(set model m)
(set shortcutHandler (new FlxKeyShortcutHandler))
(let [p (m.totalPoints)
&mut i 0 ]
// Find, load, and add the current reward image as big as possible:
(unless currentRewardFile
(set currentRewardFile (nth m.rewardFiles 0))
(while (> p .startingPoints (nth m.rewardFiles i))
(set rewardFileIndex i)
(set currentRewardFile (nth m.rewardFiles i))
(set maxRewardFile i)
(if (>= ++i m.rewardFiles.length)
(break))))
(set save (new FlxSave))
(save.bind currentRewardFile.path)
(unless save.data.storedPositions
(set save.data.storedPositions (new Map<Int,FlxPoint>)))
(unless save.data.backgroundIndex
(set save.data.backgroundIndex 0))
(set pieceCamera.bgColor (nth backgroundOptions save.data.backgroundIndex))
(let [rewardSprite
(new FlxSprite 0 0
(BitmapData.fromFile
currentRewardFile.path))]
(when rewardSprites
(remove rewardSprites))
(rewardSprite.setGraphicSize FlxG.width 0)
(rewardSprite.updateHitbox)
(when (> rewardSprite.height FlxG.height)
(rewardSprite.setGraphicSize 0 FlxG.height))
(rewardSprite.updateHitbox)
(rewardSprite.screenCenter)
(set pieceCamera.zoom rewardSprite.scale.x)
(set rewardSprites (new FlxTypedGroup))
(doFor map [matchingPiecesLeft matchingPiecesRight matchingPiecesUp matchingPiecesDown]
(map.clear))
(connectedPieces.clear)
(doFor i (range TOTAL_PIECES) (dictSet connectedPieces i []))
(indexMap.clear)
(let [r (new FlxRandom (Strings.hashCode currentRewardFile.path))
graphicWidth rewardSprite.pixels.width
graphicHeight rewardSprite.pixels.height
pieceAssetWidth (Std.int (/ graphicWidth PUZZLE_WIDTH))
pieceAssetHeight (Std.int (/ graphicHeight PUZZLE_HEIGHT))
j (new Jigsawx pieceAssetWidth pieceAssetHeight EDGE_LEEWAY BUBBLE_SIZE PUZZLE_HEIGHT PUZZLE_WIDTH r)
PIECE_WIDTH
(/ rewardSprite.width PUZZLE_WIDTH)
PIECE_HEIGHT
(/ rewardSprite.height PUZZLE_HEIGHT)
:Array<FlxPoint> startingPoints []]
(let [&mut i 0]
(doFor y (range PUZZLE_HEIGHT)
(doFor x (range PUZZLE_WIDTH)
(startingPoints.push
(new FlxPoint (+ rewardSprite.x (* x PIECE_WIDTH)) (+ rewardSprite.y (* y PIECE_HEIGHT))))
(+= i 1))))
(r.shuffle startingPoints)
(set jigsaw j)
(r.shuffle jigsaw.jigs)
(localVar spriteGrid (for y (range PUZZLE_HEIGHT) (for x (range PUZZLE_WIDTH) null)))
(localVar indexGrid (for y (range PUZZLE_HEIGHT) (for x (range PUZZLE_WIDTH) 0)))
(doFor i (range (min TOTAL_PIECES (- p currentRewardFile.startingPoints)))
(let [jig (nth jigsaw.jigs i)
pos (ifLet [point (dictGet (the Map<Int,FlxPoint> save.data.storedPositions) i)]
point
(nth startingPoints i))
s (new FlxExtendedSprite pos.x pos.y)
source (new FlxSprite)
mask (new FlxSprite)
sourceRect (new Rectangle jig.xy.x jig.xy.y jig.wh.x jig.wh.y)]
(set s.priorityID i)
(setNth spriteGrid jig.row jig.col s)
(setNth indexGrid jig.row jig.col i)
(dictSet pieceData i jig)
(dictSet indexMap s i)
(set s.draggable true)
(s.enableMouseDrag false true)
(set s.mouseStartDragCallback
->:Void [s x y]
{
// Bring currently held pieces to the front:
(rewardSprites.bringToFront s)
(set s.priorityID (+ 1 .priorityID (last (the kiss.List<FlxExtendedSprite> rewardSprites.members))))
(doFor connected (recursivelyConnectedPieces s)
(rewardSprites.bringToFront connected))
(set draggingSprite s)
(set draggingLastPos (new FlxPoint s.x s.y))
})
(set s.mouseStopDragCallback
->:Void [s x y]
{
(set draggingSprite null)
(checkMatches i)
(dictSet (the Map<Int,FlxPoint> save.data.storedPositions) i (new FlxPoint s.x s.y))
(doFor connected (recursivelyConnectedPieces s)
(checkMatches (dictGet indexMap connected))
(dictSet (the Map<Int,FlxPoint> save.data.storedPositions) (dictGet indexMap connected) (new FlxPoint connected.x connected.y)))
(pieceCamera.calculateScrollBounds rewardSprites SCROLL_BOUND_MARGIN)
(save.flush)
})
(source.makeGraphic (Std.int sourceRect.width) (Std.int sourceRect.height) FlxColor.TRANSPARENT true)
(source.pixels.copyPixels rewardSprite.pixels sourceRect (new Point 0 0))
(mask.makeGraphic (Std.int sourceRect.width) (Std.int sourceRect.height) FlxColor.TRANSPARENT true)
(drawPieceShape mask jig FlxColor.BLACK)
(FlxSpriteUtil.alphaMask s source.pixels mask.pixels)
(set s.cameras [pieceCamera])
(#when debug
(let [matchZones [(matchZoneLeft s) (matchZoneRight s)(matchZoneUp s)(matchZoneDown s)]]
(doFor z matchZones
(FlxSpriteUtil.drawRect s (- z.x s.x) (- z.y s.y) z.width z.height FlxColor.TRANSPARENT (object thickness 1 color FlxColor.RED)))))
(rewardSprites.add s)))
(doFor row (range PUZZLE_HEIGHT)
(doFor col (range PUZZLE_WIDTH)
(let [id (nth indexGrid row col)]
// combination of try/whenLet should cover target languages
// where out-of-bounds nth throws an error AND languages
// where it returns null
(try (whenLet [toLeft (nth spriteGrid row (- col 1))]
(dictSet matchingPiecesLeft id toLeft)) (catch [e] null))
(try (whenLet [toRight (nth spriteGrid row (+ col 1))]
(dictSet matchingPiecesRight id toRight)) (catch [e] null))
(try (whenLet [toUp (nth spriteGrid (- row 1) col)]
(dictSet matchingPiecesUp id toUp)) (catch [e] null))
(try (whenLet [toDown (nth spriteGrid (+ row 1) col)]
(dictSet matchingPiecesDown id toDown)) (catch [e] null)))))
(add rewardSprites)
(doFor i (range TOTAL_PIECES)
(checkMatches i))))
(pieceCamera.calculateScrollBounds rewardSprites SCROLL_BOUND_MARGIN)
(when entryTexts (remove entryTexts))
(set entryTexts (new FlxTypedGroup))
(set textY 0)
(set _color FlxColor.LIME)
(_makeText "Puzzle #$(+ 1 rewardFileIndex) / ${model.rewardFiles.length}" (- TOTAL_PIECES (- p currentRewardFile.startingPoints)))
(set _color FlxColor.ORANGE)
(map (m.activeDailyEntries) makeText)
(set _color FlxColor.GREEN)
(map (m.activeMonthlyEntries) makeText)
(set _color FlxColor.BLUE)
(map (m.activeIntervalEntries) makeText)
(set _color FlxColor.WHITE)
(map (m.activeBonusEntries) makeText)
(set _color FlxColor.YELLOW)
(map (m.activeTodoEntries) makeText)
(set _color FlxColor.GRAY)
(_makeText "[SPACE] Cycle background color" 0)
(add entryTexts))
(doFor e (m.allEntries)
(when (HabitModel.isActive e)
(let [label (HabitModel.activeLabel e)]
(shortcutHandler.registerItem label.label e))))
(set shortcutHandler.onBadKey ->:Void [_ _] {})
(set shortcutHandler.onSelectItem ->:Void [:Entry e]
(let [label (HabitModel.activeLabel e)]
(+= label.points 1)
(whenLet [(Daily days lastDayDone) e.type]
(set e.type (Daily days (HabitModel.todayString))))
(whenLet [(Monthly days lastDayDone) e.type]
(set e.type (Monthly days (.toString (DateTime.now)))))
(whenLet [(Interval days lastDayDone) e.type]
(set e.type (Interval days (.toString (DateTime.now)))))
(m.save)
(setModel m)
(shortcutHandler.start)))
(shortcutHandler.start))
(prop &mut textY 0)
// Color currently used for making text, may be inverted or lightened to contrast with background:
(prop &mut :FlxColor _color FlxColor.BLACK)
(method getColor []
(if (= _color pieceCamera.bgColor)
(if (= pieceCamera.bgColor FlxColor.GRAY)
(_color.getLightened 0.3)
(_color.getInverted))
_color))
(defAlias &ident color (getColor))
(prop :Array<FlxColor> backgroundOptions [
FlxColor.BLACK
FlxColor.WHITE
FlxColor.GRAY
])
(method makeText [:Entry e]
(let [label (HabitModel.activeLabel e)]
(_makeText label.label label.points)))
// TODO configurable text size
(method _makeText [:String s :Int points]
(let [text (new FlxText 0 textY 0 (+ s (* points "+")) (defAndReturn var &mut textSize 16))]
(set text.color color)
(set text.cameras [uiCamera])
(+= textY text.height)
(entryTexts.add text)))
(method :FlxRect matchZoneLeft [:FlxExtendedSprite s]
(new FlxRect s.x (+ s.y s.origin.y) EDGE_LEEWAY EDGE_LEEWAY))
(method :FlxRect matchZoneRight [:FlxExtendedSprite s]
(new FlxRect (- (+ s.x s.width) EDGE_LEEWAY) (+ s.y s.origin.y) EDGE_LEEWAY EDGE_LEEWAY))
(method :FlxRect matchZoneUp [:FlxExtendedSprite s]
(new FlxRect (+ s.x s.origin.x) s.y EDGE_LEEWAY EDGE_LEEWAY))
(method :FlxRect matchZoneDown [:FlxExtendedSprite s]
(new FlxRect (+ s.x s.origin.x) (- (+ s.y s.height) EDGE_LEEWAY) EDGE_LEEWAY EDGE_LEEWAY))
(prop &mut c 0)
(method :Void connectPiece [id self toSprite]
(let [thisConnectedPieces (dictGet connectedPieces id)
toConnectedPieces (dictGet connectedPieces (dictGet indexMap toSprite))]
(+= c 1)
// Don't add duplicates
(thisConnectedPieces.remove toSprite)
(thisConnectedPieces.push toSprite)
(toConnectedPieces.remove self)
(toConnectedPieces.push self)))
(method :Void checkMatches [id]
(when !(pieceData.exists id) (return))
(let [s (nth rewardSprites.members id)
jig (dictGet pieceData id)
row jig.row
col jig.col]
/* // TODO tune the match zones
(let [l (matchZoneLeft s)
r (matchZoneRight s)]
(s.drawRect (- l.x s.x) (- l.y s.y) l.width l.height)
(s.drawRect (- r.x s.x) (- r.y s.y) r.width r.height)) */
(whenLet [toLeft (dictGet matchingPiecesLeft id)
mzl (matchZoneLeft s)
mzr (matchZoneRight toLeft)]
(unless .isEmpty (mzl.intersection mzr)
(connectPiece id s toLeft)))
(whenLet [toRight (dictGet matchingPiecesRight id)
mzr (matchZoneRight s)
mzl (matchZoneLeft toRight)]
(unless .isEmpty (mzl.intersection mzr)
(connectPiece id s toRight)))
(whenLet [toUp (dictGet matchingPiecesUp id)
mzu (matchZoneUp s)
mzd (matchZoneDown toUp)]
(unless .isEmpty (mzu.intersection mzd)
(connectPiece id s toUp)))
(whenLet [toDown (dictGet matchingPiecesDown id)
mzd (matchZoneDown s)
mzu (matchZoneUp toDown)]
(unless .isEmpty (mzu.intersection mzd)
(connectPiece id s toDown)))))
(method :Array<FlxExtendedSprite> recursivelyConnectedPieces [s &opt :Array<FlxExtendedSprite> collected]
(unless collected (set collected []))
(whenLet [directlyConnected (dictGet connectedPieces (dictGet indexMap s))]
(doFor piece directlyConnected
(unless (contains collected piece)
(collected.push piece)
(recursivelyConnectedPieces piece collected))))
collected)