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

551 lines
26 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")
(defAlias &ident textSize SimpleWindow.textSize)
(method &override :Void create []
(add logTexts)
(set Prelude.printStr log)
(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))
(defAlias &ident KEYBOARD_SCROLL_SPEED (keyboardScrollSpeed))
(method keyboardScrollSpeed []
(/ 800 rewardSprite.scale.x))
(prop &mut :EntryType typeAdding Todo)
(prop &mut :FlxInputText entryNameText)
(prop &mut :FlxSprite cameraBounds null)
(prop &mut :Float timer 0)
(method &override :Void update [:Float elapsed]
(super.update elapsed)
**(#when debug
(+= timer elapsed)
(when (> timer 0.5)
(set timer 0)
(unless cameraBounds
(set cameraBounds (new FlxSprite))
(set cameraBounds.cameras [pieceCamera])
(add cameraBounds))
(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)))
(doFor s rewardSprites
//(s.drawCircle s.origin.x s.origin.y 5 FlxColor.LIME)
(cameraBounds.drawCircle (- s.x cameraBounds.x) (- s.y cameraBounds.y) 5 FlxColor.LIME)
// Uncomment for debugging match zones:
(let [matchZones [(matchZoneLeft s) (matchZoneRight s)(matchZoneUp s)(matchZoneDown s)]]
(doFor z matchZones
(unless z.isEmpty
(FlxSpriteUtil.drawRect cameraBounds (- z.x cameraBounds.x) (- z.y cameraBounds.y) z.width z.height FlxColor.TRANSPARENT (object thickness 1 color FlxColor.RED))))))))
(pieceCamera.updateScrollWheelZoom elapsed 5)
(pieceCamera.updateMouseBorderControl elapsed KEYBOARD_SCROLL_SPEED 0.002 uiCamera)
(when entryWindow
(when FlxG.keys.justPressed.ESCAPE
(if (entryWindow.isShown)
(entryWindow.hide)
(entryWindow.show))))
(#when debug
(when FlxG.keys.justPressed.SEMICOLON
(set pieceCamera.zoom 1))
(when FlxG.keys.justPressed.CONTROL
(set save.data.storedPositions (new Map<Int,FlxPoint>))
(set save.data.storedAngles (new Map<Int,Float>))
(set save.data.storedOrigins (new Map<Int,FlxPoint>))
(save.flush)))
(when FlxG.keys.justPressed.DELETE
(Sys.exit 0))
// TODO provide a saner/configurable set of bindings to trigger these ui action functions
{
(when FlxG.keys.justPressed.Q
(when draggingSprite
(draggingSprite.rotate 90)
(doFor s (recursivelyConnectedPieces draggingSprite)
(dictSet (the Map<Int,Float> save.data.storedAngles) (dictGet indexMap s) s.angle)
(dictSet (the Map<Int,FlxPoint> save.data.storedOrigins) (dictGet indexMap s) s.origin))
(save.flush)))
(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))
}
// 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)))))
(prop &mut :FlxSave save null)
(prop &mut :SimpleWindow entryWindow null)
(prop &mut :FlxTypedGroup<FlxText> logTexts (new FlxTypedGroup))
(prop &mut :FlxKeyShortcutHandler<Entry> shortcutHandler null)
(prop &mut :HabitModel model null)
(method roughOptimalScale []
(/ 367 (smallerDimension)))
(method smallerDimension [] (min rewardSprite.pixels.width rewardSprite.pixels.height))
// TODO these variables don't do exactly what I think they do when scaled, like at all:
(defAlias &ident EDGE_LEEWAY 25)
(defAlias &ident BUBBLE_SIZE 15)
(defAlias &ident PUZZLE_WIDTH .puzzleWidth (nth model.rewardFiles rewardFileIndex))
(defAlias &ident PUZZLE_HEIGHT .puzzleHeight (nth model.rewardFiles rewardFileIndex))
(defAlias &ident TOTAL_PIECES (* PUZZLE_WIDTH PUZZLE_HEIGHT))
(prop &mut :FlxSprite rewardSprite null)
(prop &mut :FlxTypedGroup<KissExtendedSprite> rewardSprites null)
(prop &mut :Map<Int,KissExtendedSprite> matchingPiecesLeft (new Map))
(prop &mut :Map<Int,KissExtendedSprite> matchingPiecesRight (new Map))
(prop &mut :Map<Int,KissExtendedSprite> matchingPiecesUp (new Map))
(prop &mut :Map<Int,KissExtendedSprite> matchingPiecesDown (new Map))
(prop &mut :Map<Int,JigsawPiece> pieceData (new Map))
(prop &mut :Map<Int,Array<KissExtendedSprite>> connectedPieces (new Map))
(prop &mut :Map<KissExtendedSprite,Int> indexMap (new Map))
(prop &mut :Map<Int,KissExtendedSprite> 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 []
(/ (max FlxG.width FlxG.height) rewardSprite.scale.x))
(prop &mut :KissExtendedSprite 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
&mut puzzleUnlocked -1]
// 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)
--i
(let [lastStartingPoints .startingPoints (nth m.rewardFiles i)
piecesPerPoint .piecesPerPoint (nth m.rewardFiles i)
nextStartingPoints (+ lastStartingPoints (Math.ceil (/ TOTAL_PIECES piecesPerPoint)))]
(when (> p nextStartingPoints)
(set puzzleUnlocked nextStartingPoints))
(break)))))
(set save (new FlxSave))
(save.bind currentRewardFile.path)
(unless save.data.storedPositions
(set save.data.storedPositions (new Map<Int,FlxPoint>)))
(unless save.data.storedAngles
(set save.data.storedAngles (new Map<Int,Float>)))
(unless save.data.storedOrigins
(set save.data.storedOrigins (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))
ros (roughOptimalScale)
graphicWidth (* ros rewardSprite.pixels.width)
graphicHeight (* ros rewardSprite.pixels.height)
pieceAssetWidth (/ (- graphicWidth (* EDGE_LEEWAY 2)) PUZZLE_WIDTH)
pieceAssetHeight (/ (- graphicHeight (* EDGE_LEEWAY 2)) PUZZLE_HEIGHT)
j (new Jigsawx pieceAssetWidth pieceAssetHeight graphicWidth graphicHeight EDGE_LEEWAY BUBBLE_SIZE PUZZLE_HEIGHT PUZZLE_WIDTH r)
PIECE_WIDTH
(/ rewardSprite.width PUZZLE_WIDTH)
PIECE_HEIGHT
(/ rewardSprite.height PUZZLE_HEIGHT)
:Array<FlxPoint> startingPoints []
:Array<Float> startingAngles []]
(let [&mut i 0]
(doFor y (range PUZZLE_HEIGHT)
(doFor x (range PUZZLE_WIDTH)
(startingAngles.push (* 90 (r.int 0 3)))
(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) -1)))
(doFor i (range (min TOTAL_PIECES (* currentRewardFile.piecesPerPoint (- 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))
angle (ifLet [angle (dictGet (the Map<Int,Float> save.data.storedAngles) i)]
angle
(nth startingAngles i))
s (new KissExtendedSprite pos.x pos.y)
source (new FlxSprite)
mask (new FlxSprite)
sourceRect (new Rectangle (/ jig.xy.x ros) (/ jig.xy.y ros) (/ jig.wh.x ros) (/ jig.wh.y ros))]
(set s.angle angle)
(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]
(let [s (cast s KissExtendedSprite)]
(set s.priorityID (+ 1 .priorityID (last (the kiss.List<KissExtendedSprite> rewardSprites.members))))
(let [connectedPieces (recursivelyConnectedPieces s)]
// Bring currently held pieces to the front:
(rewardSprites.bringAllToFront connectedPieces)
(set s.connectedSprites connectedPieces))
(set draggingSprite s)
(set draggingLastPos (new FlxPoint s.x s.y))))
(set s.mouseStopDragCallback
->:Void [s x y]
(let [s (cast s KissExtendedSprite)]
(set draggingSprite null)
(let [connectedPieces (recursivelyConnectedPieces s)]
(doFor connected connectedPieces
(checkMatches (dictGet indexMap connected)))
(doFor connected connectedPieces
(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)))
(var ROT_PADDING 2)
(localVar fWidth (+ (Std.int sourceRect.width) (* 2 ROT_PADDING)))
(localVar fHeight (+ (Std.int sourceRect.height) (* 2 ROT_PADDING)))
(source.makeGraphic fWidth fHeight FlxColor.TRANSPARENT true)
(source.pixels.copyPixels rewardSprite.pixels sourceRect (new Point ROT_PADDING ROT_PADDING))
(mask.makeGraphic fWidth fHeight FlxColor.TRANSPARENT true)
(drawPieceShape mask jig ros FlxColor.BLACK)
(FlxSpriteUtil.alphaMask s source.pixels mask.pixels)
**(#when debug
(kiss_flixel.SpriteTools.writeOnSprite "$i" 32 s (object x (Percent 0.5) y (Percent 0.5)) FlxColor.RED))
(s.loadRotatedGraphic s.pixels 4 -1 /*false true*/)
(set s.cameras [pieceCamera])
(rewardSprites.add s)))
(doFor row (range PUZZLE_HEIGHT)
(doFor col (range PUZZLE_WIDTH)
(let [id (nth indexGrid row col)]
(when (= id -1) (continue))
(when (>= (- col 1) 0)
(let [toLeft (nth spriteGrid row (- col 1))]
(dictSet matchingPiecesLeft id toLeft)))
(when (< (+ col 1) PUZZLE_WIDTH)
(let [toRight (nth spriteGrid row (+ col 1))]
(dictSet matchingPiecesRight id toRight)))
(when (>= (- row 1) 0)
(let [toUp (nth spriteGrid (- row 1) col)]
(dictSet matchingPiecesUp id toUp)))
(when (< (+ row 1) PUZZLE_HEIGHT)
(let [toDown (nth spriteGrid (+ row 1) col)]
(dictSet matchingPiecesDown id toDown))))))
(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)
(localVar &mut windowWasShown true)
(when entryWindow
(set windowWasShown (entryWindow.isShown))
(entryWindow.hide))
(set entryWindow (new SimpleWindow "" (FlxColor.fromRGBFloat 0 0 0 0.5) FlxColor.WHITE 0.9 0.9))
(set entryWindow.cameras [uiCamera])
(set entryWindow.textColor FlxColor.LIME)
(_makeText "Puzzle #$(+ 1 rewardFileIndex) / ${model.rewardFiles.length}" (max 0 (- TOTAL_PIECES (* currentRewardFile.piecesPerPoint (- p currentRewardFile.startingPoints)))))
(set entryWindow.textColor FlxColor.ORANGE)
(map (m.activeDailyEntries) makeText)
(set entryWindow.textColor FlxColor.GREEN)
(map (m.activeMonthlyEntries) makeText)
(set entryWindow.textColor FlxColor.BLUE)
(map (m.activeIntervalEntries) makeText)
(set entryWindow.textColor FlxColor.WHITE)
(map (m.activeBonusEntries) makeText)
(set entryWindow.textColor FlxColor.YELLOW)
(map (m.activeTodoEntries) makeText)
(set entryWindow.textColor FlxColor.GRAY)
(_makeText "[SPACE] Cycle background color" 0)
(when windowWasShown
(entryWindow.show))
(when !(= puzzleUnlocked -1)
(startPuzzlePackChoice puzzleUnlocked))))
(method refreshModel [&opt m]
(let [m (or m model)]
(setModel m (nth m.rewardFiles rewardFileIndex))))
(prop &mut textY 0)
(prop :Array<FlxColor> backgroundOptions [
FlxColor.BLACK
FlxColor.WHITE
FlxColor.GRAY
])
(method makeText [:Entry e]
(let [label (HabitModel.activeLabel e)]
(_makeText label.label label.points ->:Void text {
// TODO move all of this logic other than setModel into HabitModel logic
(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)))))
(model.save)
(setModel model))
})))
// TODO configurable text size
(method _makeText [:String s :Int points &opt :Action action]
(entryWindow.makeText (+ s (pointsStr points)) action))
(method log [:String message]
(trace 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 FlxColor.LIME)
(set text.cameras [uiCamera])
(+= logTextY text.height)
(-= text.x text.width)
(logTexts.add text))))
(method :FlxRect matchZone [:KissExtendedSprite s compass]
(let [bubblePoints (dictGet .bubblePoints (dictGet pieceData (dictGet indexMap s)) compass)]
(unless bubblePoints
(return (new FlxRect 0 0 0 0)))
(let [ros (roughOptimalScale)
pointsX (for point bubblePoints point.x)
pointsY (for point bubblePoints point.y)
minX (/ (apply min pointsX) ros)
minY (/ (apply min pointsY) ros)
maxX (/ (apply max pointsX) ros)
maxY (/ (apply max pointsY) ros)
tlc (.add (new FlxPoint minX minY) ROT_PADDING ROT_PADDING)
brc (.add (new FlxPoint maxX maxY) ROT_PADDING ROT_PADDING)
rotationPadding (s.getRotationPadding)
rect (.fromTwoPoints (new FlxRect) (tlc.addPoint rotationPadding) (brc.addPoint rotationPadding))
originOffset (new FlxPoint (- s.origin.x rect.x) (- s.origin.y rect.y))
rotated (rect.getRotatedBounds s.angle originOffset)]
(+= rotated.x s.x)
(+= rotated.y s.y)
rotated)))
(method :FlxRect matchZoneLeft [:KissExtendedSprite s]
(matchZone s WEST))
(method :FlxRect matchZoneRight [:KissExtendedSprite s]
(matchZone s EAST))
(method :FlxRect matchZoneUp [:KissExtendedSprite s]
(matchZone s NORTH))
(method :FlxRect matchZoneDown [:KissExtendedSprite s]
(matchZone s SOUTH))
(prop &mut c 0)
(method :Bool connectPiece [id self toSprite selfMatchZone toSpriteMatchZone]
(let [thisConnectedPieces (dictGet connectedPieces id)
toConnectedPieces (dictGet connectedPieces (dictGet indexMap toSprite))]
// Don't add duplicates or snap for pieces alread connected
(when (contains thisConnectedPieces toSprite)
(return false))
(+= c 1)
// Snap the pieces together
(let [offsetX (- toSpriteMatchZone.x selfMatchZone.x)
offsetY (- toSpriteMatchZone.y selfMatchZone.y)
selfAndAttached (recursivelyConnectedPieces self)
indices (for s selfAndAttached (dictGet indexMap s))
otherAndAttached (recursivelyConnectedPieces toSprite)
otherIndices (for s otherAndAttached (dictGet indexMap s))]
//(print "attaching $indices to $otherIndices")
(doFor piece selfAndAttached
(+= piece.x offsetX)
(+= piece.y offsetY))
// TODO check for matches created by snapping all the pieces?
// Or is it fine not to?
)
(thisConnectedPieces.push toSprite)
(toConnectedPieces.push self)
true))
(defMacro _checkMatch [side otherSide]
(let [sideStr (symbolNameValue side)
otherSideStr (symbolNameValue otherSide)
to (symbol "to$sideStr")
mp (symbol "matchingPieces$sideStr")
mz1 (symbol "matchZone$sideStr")
mz2 (symbol "matchZone$otherSideStr")]
`(whenLet [,to (dictGet ,mp id)
mz1 (,mz1 s)
mz2 (,mz2 ,to)]
(unless (or !(= s.angle .angle ,to) .isEmpty (mz1.intersection mz2))
(connectPiece id s ,to mz1 mz2)))))
(method :Bool checkMatches [id]
(when !(pieceData.exists id) (return false))
(let [s (dictGet spriteMap id)
jig (dictGet pieceData id)
row jig.row
col jig.col]
(_checkMatch Left Right)
(_checkMatch Right Left)
(_checkMatch Up Down)
(_checkMatch Down Up))
false)
(method :Array<KissExtendedSprite> recursivelyConnectedPieces [s &opt :Array<KissExtendedSprite> collected]
(unless collected (set collected [s]))
(whenLet [directlyConnected (dictGet connectedPieces (dictGet indexMap s))]
(doFor piece directlyConnected
(unless (contains collected piece)
(collected.push piece)
(recursivelyConnectedPieces piece collected))))
collected)
(prop &mut :FlxGroup nextPuzzleChoiceGroup null)
(var BASE_PUZZLE_SIZE 5)
(method startPuzzlePackChoice [nextStartingPoints]
(unless nextPuzzleChoiceGroup
(set nextPuzzleChoiceGroup (new FlxGroup))
(set nextPuzzleChoiceGroup.cameras [uiCamera])
// TODO position these aesthetically with a partly transparent background behind them
// like the habit ui window should also have
(let [x 0 &mut y 0]
(doFor pack (availablePacks model)
(let [text (new FlxText x y 0 "$(haxe.io.Path.withoutDirectory pack.path): ${pack.puzzlesDone}/${pack.puzzlesTotal}" textSize)]
// TODO not that color though
(set text.color FlxColor.LIME)
(nextPuzzleChoiceGroup.add text)
(whenLet [(Some np) pack.nextPuzzle]
(nextPuzzleChoiceGroup.add (new FlxButton (+ x text.width) y "CHOOSE" ->:Void {
(remove nextPuzzleChoiceGroup)
(set nextPuzzleChoiceGroup null)
(let [bmd (BitmapData.fromFile np.path)
aspectRatioX (/ bmd.width bmd.height)
aspectRatioY (/ bmd.height bmd.width)
w (max 1 (Math.round (* aspectRatioX BASE_PUZZLE_SIZE)))
h (max 1 (Math.round (* aspectRatioY BASE_PUZZLE_SIZE)))]
(model.addRewardFile np.path nextStartingPoints w h 1))
(setModel model)
}))))
(+= y textSize)))
(add nextPuzzleChoiceGroup)))
(function pointsStr [points]
(let [tallyUnit 5]
(+ (* "*" (Math.floor (/ points tallyUnit)))
(* "+" (% points tallyUnit)))))