(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 bgColor FlxColor.TRANSPARENT) (super.create)) (var KEYBOARD_SCROLL_SPEED 200) (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)) (when FlxG.keys.justPressed.SPACE (set save.data.backgroundIndex #{(save.data.backgroundIndex + 1) % backgroundOptions.length;}#) (save.flush) // setModel so the entry text gets remade in inverted colors (setModel model (nth model.rewardFiles rewardFileIndex))) // 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 (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 shortcutHandler (shortcutHandler.update))) (prop &mut :FlxSave save null) (prop &mut :FlxTypedGroup entryTexts null) (prop &mut :FlxKeyShortcutHandler 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 rewardSprites null) (prop &mut :Map matchingPiecesLeft (new Map)) (prop &mut :Map matchingPiecesRight (new Map)) (prop &mut :Map matchingPiecesUp (new Map)) (prop &mut :Map matchingPiecesDown (new Map)) (prop &mut :Map pieceData (new Map)) (prop &mut :Map> connectedPieces (new Map)) (prop &mut :Map 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))) (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 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 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)] (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) (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 save.data.storedPositions) i (new FlxPoint s.x s.y)) (doFor connected (recursivelyConnectedPieces s) (checkMatches (dictGet indexMap connected)) (dictSet (the Map 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 backgroundOptions [ FlxColor.BLACK FlxColor.WHITE FlxColor.GRAY ]) (method makeText [:Entry e] (let [label (HabitModel.activeLabel e)] (_makeText label.label label.points))) (method _makeText [:String s :Int points] (let [text (new FlxText 0 textY 0 (+ s (* points "+")) 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 recursivelyConnectedPieces [s &opt :Array collected] (unless collected (set collected [])) (let [directlyConnected (dictGet connectedPieces (dictGet indexMap s))] (doFor piece directlyConnected (unless (contains collected piece) (collected.push piece) (recursivelyConnectedPieces piece collected)))) collected)