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

477 lines
21 KiB
Plaintext

(loadFrom "kiss-tools" "src/kiss_tools/RefactorUtil.kiss")
(prop &mut :Jigsawx jigsaw)
(prop &mut :FlxCamera pieceCamera)
(prop &mut :FlxCamera uiCamera)
(load "PuzzlePacks.kiss")
(method &override :Void create []
(#when debug
(add cameraBounds))
(add logTexts)
(set Prelude.printStr log)
(set pieceCamera FlxG.camera)
(set cameraBounds.cameras [pieceCamera])
(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))
(defAlias &ident KEYBOARD_SCROLL_SPEED (keyboardScrollSpeed))
(method keyboardScrollSpeed []
(/ 200 rewardSprite.scale.x))
(prop &mut :EntryType typeAdding Todo)
(prop &mut :FlxInputText entryNameText)
(prop :FlxSprite cameraBounds (new FlxSprite))
(method &override :Void update [:Float elapsed]
(super.update elapsed)
(#when debug
(let [b (pieceCamera.getScrollBounds)]
(set cameraBounds.x b.x)
(set cameraBounds.y b.y)
(cameraBounds.makeGraphic (Std.int b.width) (Std.int b.height) FlxColor.TRANSPARENT true)
(cameraBounds.drawRect 0 0 b.width b.height FlxColor.TRANSPARENT (object color FlxColor.LIME thickness 5))))
(pieceCamera.updateScrollWheelZoom elapsed 5)
(pieceCamera.updateMouseBorderControl elapsed KEYBOARD_SCROLL_SPEED 0.002 uiCamera)
// 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
(refreshModel)))
(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
// addEntry() calls save()
(model.addEntry typeAdding [entryNameText.text])
(refreshModel)
(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))))
// Left and right arrow keys can switch between unlocked puzzles
(unless entryNameText
(when FlxG.keys.justPressed.LEFT
(unless (= rewardFileIndex 0)
(-= rewardFileIndex 1)
(refreshModel)))
(when FlxG.keys.justPressed.RIGHT
(unless (= rewardFileIndex maxRewardFile)
(+= rewardFileIndex 1)
(refreshModel))))
// Handle keyboard input:
(when (and shortcutHandler !entryNameText)
(shortcutHandler.update)))
(prop &mut :FlxSave save null)
(prop &mut :FlxTypedGroup<FlxText> entryTexts null)
(prop &mut :FlxTypedGroup<FlxText> logTexts (new FlxTypedGroup))
(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 :FlxSprite rewardSprite null)
(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 :Map<Int,FlxExtendedSprite> spriteMap (new Map)) // Because rewardSprites will be re-ordered in depth handling, this is required
(prop &mut lastRewardFileIndex -1)
(prop &mut rewardFileIndex 0)
(prop &mut maxRewardFile 0)
(defAlias &ident SCROLL_BOUND_MARGIN (scrollBoundMargin))
(method scrollBoundMargin []
(fHalf (/ (max FlxG.width FlxG.height) rewardSprite.scale.x)))
(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)
(when (>= ++i m.rewardFiles.length)
(let [lastStartingPoints .startingPoints (nth m.rewardFiles --i)
nextStartingPoints (+ lastStartingPoints TOTAL_PIECES)]
(if (> p nextStartingPoints)
{
// TODO give the player a puzzle pack choice using (availablePacks)
(m.addRewardFile .path (firstUnsolvedPuzzle m "puzzles/trentnelson") nextStartingPoints)
(setModel m)
(return)
}
(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))
(unless (and (= lastRewardFileIndex rewardFileIndex) (= lastTotalPoints (m.totalPoints)))
(set 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)
(dictSet spriteMap i s)
(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 uiCamera 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
// Uncomment for debugging match zones:
**(let [matchZones [(matchZoneLeft s) (matchZoneRight s)(matchZoneUp s)(matchZoneDown s)]]
(doFor z matchZones
(unless z.isEmpty
(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))))
(set lastRewardFileIndex rewardFileIndex)
(prop &mut lastTotalPoints -1)
(set lastTotalPoints (m.totalPoints))
(pieceCamera.calculateScrollBounds rewardSprites uiCamera 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))
(method refreshModel [&opt m]
(let [m (or m model)]
(setModel m (nth m.rewardFiles rewardFileIndex))))
(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 log [message]
(prop &mut logTextY 0)
(#when debug
(when (> logTextY FlxG.height)
(logTexts.clear)
(set logTextY 0))
(let [text (new FlxText FlxG.width logTextY 0 message textSize)]
(set text.color color)
(set text.cameras [uiCamera])
(+= logTextY text.height)
(-= text.x text.width)
(logTexts.add text))))
// TODO properly tune the match zones to bubbles
(method :FlxRect matchZone [:FlxExtendedSprite s compass]
(let [bubblePoints (dictGet .bubblePoints (dictGet pieceData (dictGet indexMap s)) compass)]
(unless bubblePoints
(return (new FlxRect 0 0 0 0)))
(let [pointsX (for point bubblePoints point.x)
pointsY (for point bubblePoints point.y)
minX (apply min pointsX)
minY (apply min pointsY)
maxX (apply max pointsX)
maxY (apply max pointsY)
rect (.fromTwoPoints (new FlxRect) (new FlxPoint minX minY) (new FlxPoint maxX maxY))]
(+= rect.x s.x)
(+= rect.y s.y)
rect)))
(method :FlxRect matchZoneLeft [:FlxExtendedSprite s]
(matchZone s WEST))
(method :FlxRect matchZoneRight [:FlxExtendedSprite s]
(matchZone s EAST))
(method :FlxRect matchZoneUp [:FlxExtendedSprite s]
(matchZone s NORTH))
(method :FlxRect matchZoneDown [:FlxExtendedSprite s]
(matchZone s SOUTH))
(prop &mut c 0)
(method :Void connectPiece [id self toSprite selfMatchZone toSpriteMatchZone]
(let [thisConnectedPieces (dictGet connectedPieces id)
toConnectedPieces (dictGet connectedPieces (dictGet indexMap toSprite))]
(+= c 1)
// Snap the pieces together
(let [offsetX (- toSpriteMatchZone.x selfMatchZone.x)
offsetY (- toSpriteMatchZone.y selfMatchZone.y)]
(doFor piece (.concat (recursivelyConnectedPieces self) [self])
~"snip snap"
(+= piece.x offsetX)
(+= piece.y offsetY))
// TODO check for matches created by snapping all the pieces?
// Or is it fine not to?
)
// 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 (dictGet spriteMap id)
jig (dictGet pieceData id)
row jig.row
col jig.col]
(whenLet [toLeft (dictGet matchingPiecesLeft id)
mzl (matchZoneLeft s)
mzr (matchZoneRight toLeft)]
(unless .isEmpty (mzl.intersection mzr)
(connectPiece id s toLeft mzl mzr)))
(whenLet [toRight (dictGet matchingPiecesRight id)
mzr (matchZoneRight s)
mzl (matchZoneLeft toRight)]
(unless .isEmpty (mzl.intersection mzr)
(connectPiece id s toRight mzr mzl)))
(whenLet [toUp (dictGet matchingPiecesUp id)
mzu (matchZoneUp s)
mzd (matchZoneDown toUp)]
(unless .isEmpty (mzu.intersection mzd)
(connectPiece id s toUp mzu mzd)))
(whenLet [toDown (dictGet matchingPiecesDown id)
mzd (matchZoneDown s)
mzu (matchZoneUp toDown)]
(unless .isEmpty (mzu.intersection mzd)
(connectPiece id s toDown mzd mzu)))))
(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)