(prop :FlxActionDigital continueAction) (prop :FlxActionDigital fastForwardAction) (prop actionManager (new FlxActionManager)) (prop &mut :Movie movie) (defAlias &ident flxMovie (cast movie FlxMovie)) (loadFrom "hollywoo-flixel" "src/hollywoo_flixel/Aliases.kiss") (prop :Array> spriteLayers []) (var LAYER_MAX 8) (savedVar :Map lastSceneLabels (new Map)) (function :String lastSceneForMovie [:FlxMovie m &opt :String setValue] (let [clazz (Type.getClass m) className (Type.getClassName clazz) currentValue (dictGet lastSceneLabels className)] (if setValue (withMutProperties [lastSceneLabels] (dictSet lastSceneLabels className setValue) setValue) currentValue))) // TODO this should be part of Hollywoo's logic (method :Void promptForResume [:FlxMovie movie] (movie.createCameras) (if (.exists (movie.labelRunners) (lastSceneForMovie movie)) {(_chooseString "Resume from '$(lastSceneForMovie movie)'?" ["" "Resume" "Scene Selection" "Start From Beginning"] ->choice (case choice ("Resume" (movie.runFromLabel (lastSceneForMovie movie))) ("Scene Selection" (sceneSelection ->:Void { (FlxG.switchState (new MenuState)) })) ("Start From Beginning" (movie.run)) (never otherwise)) true "escape") (set pauseMenu.onClose ->:Void (FlxG.switchState (new MenuState)))} (movie.run))) // MenuState uses a director instance to demo the volume. This shouldn't affect flixel globals i.e. by adding an actionManager or sprite layers (defNew [&opt :Bool demoDirector] (unless demoDirector // Continue action requires fresh input, Fast Forward action supports continuous holding down (set continueAction (new FlxActionDigital "Continue" onContinue)) (set fastForwardAction (new FlxActionDigital "Fast Forward")) // TODO allow configuring continue/fast-forward keys -- any key, specifically mapped keys, etc. (doFor [:FlxActionDigital action :FlxInputState trigger] (zipThrow [continueAction fastForwardAction] [JUST_PRESSED PRESSED]) (action.addKey SPACE trigger) (action.addKey ENTER trigger) (action.addMouse LEFT trigger) (action.addGamepad FlxGamepadInputID.A trigger FlxInputDeviceID.ALL) (actionManager.addAction action)) (FlxG.inputs.add actionManager) (#when debug (kiss_flixel.DebugTools.f1ToRecord actionManager) (kiss_flixel.DebugTools.f2ToLogSprites actionManager)) (set actionManager.resetOnStateSwitch NONE) (doFor i (range (+ 1 LAYER_MAX)) (let [g (new FlxTypedGroup)] (spriteLayers.push g) (FlxG.state.add g))))) (prop :Map skySprites (new Map)) (prop &mut :FlxSprite skySprite) (prop :FlxKeyShortcutHandlerVoid> sh (new FlxKeyShortcutHandler)) (method :KeyShortcutHandlerVoid> shortcutHandler [] (set sh.cancelKey null) sh) (method :Void pause [] (FlxG.inputs.remove actionManager) (FlxG.state.forEach ->:Void child (typeCase [child] ([:FlxSprite sprite] (sprite.animation?.pause)) (otherwise)) true) (doFor sound currentSounds (sound.pause)) (doFor track currentVoiceTracks (track.pause)) (when music (music.pause))) (method resumeAndUpdateCurrentVolume [:Array sounds :Float newVolume] (doFor sound sounds (let [[original mod] (dictGet currentSoundVolumes sound)] (set sound.volume (* original mod newVolume)) (sound.resume)))) (method :Void resume [] (set FlxG.mouse.visible true) (FlxG.inputs.add actionManager) (FlxG.state.forEach ->:Void child (typeCase [child] ([:FlxSprite sprite] (sprite.animation?.resume)) (otherwise)) true) (resumeAndUpdateCurrentVolume currentSounds soundVolume) (resumeAndUpdateCurrentVolume currentVoiceTracks voiceVolume) (when music (resumeAndUpdateCurrentVolume [music] musicVolume))) (prop &mut :SimpleWindow pauseMenu null) (method :Void sceneSelection [:Continuation cancel] (let [runners (movie.labelRunners) labels (sort (collect (runners.keys))) lastLabelIndex (labels.indexOf movie.lastLabel)] (_chooseString "Skip to scene?" labels ->label { (let [m flxMovie] // This calls cleanup: (m.prepareForSkip) ((dictGet runners label) m.skipMovie)) } true "escape") (unless (= -1 lastLabelIndex) (set pauseMenu.selectedIndex (+ 1 lastLabelIndex))) (set pauseMenu.onClose cancel))) (method :Void showPauseMenu [:Continuation resume] // if fullscreen changes while paused, the movie needs to re-load when resuming (let [oldResume resume previousFullscreen MenuState.fullscreen resume ->:Void (if (= previousFullscreen MenuState.fullscreen) (oldResume) (let [m flxMovie instructionPointer (Reflect.field m "lastInstructionPointer")] (m.prepareForSkip) (m.skipMovie.runFromInstruction instructionPointer)))] // register escape to resume (and register escape to pause when resuming lol) (sh.registerItem "{escape} resume" ->cc { (pauseMenu.hide) (sh.registerItem "{escape} pause" ->cc (showPauseMenu cc) true) (resume) } true) (let [choices ["" "Resume" "Scene Selection" "Options" "Send Feedback" "Main Menu" "Quit to Desktop"] sceneSelectionIndex (choices.indexOf "Scene Selection") optsIdx (choices.indexOf "Options") feedbackIdx (choices.indexOf "Send Feedback")] // chooseString automatically sets pauseMenu (chooseString "PAUSED" choices ->:Void choice (case choice ("Resume" (sh.registerItem "{escape} pause" ->cc (showPauseMenu cc) true) (resume)) ("Scene Selection" (localFunction backToPause [] (set pauseMenu.onClose null) (pauseMenu.hide) (showPauseMenu resume) (set pauseMenu.selectedIndex sceneSelectionIndex)) (sh.registerItem "{escape} pause" ->cc (backToPause) true) (sceneSelection ->:Void { (backToPause) })) ("Options" (sh.cancel) (MenuState.optionsMenu ->{ (sh.start) (showPauseMenu resume) (set pauseMenu.selectedIndex optsIdx) } null null this)) ("Send Feedback" (sh.cancel) (.enableGamepadInput (kiss_flixel.FeedbackWindow.collectFeedback ->:Void { (sh.start) (showPauseMenu resume) (set pauseMenu.selectedIndex feedbackIdx) } null null true "escape") true)) ("Main Menu" (FlxG.switchState (new MenuState))) ("Quit to Desktop" (Sys.exit 0)) (never otherwise)))))) (function specialHistoryChars [:String text] (text.replace "✓" "")) (function specialTypesSpeakerName [:String name :SpeechType type] (case type ((VoiceOver _) "${name} (voiceover)") ((TextMessage _) "${name} (text message)") ((FromPhone _) "${name} (from phone)") ((Custom type _ _) "${name} (${type})") (otherwise name))) (method :Void showDialogHistory [:Array> history :Continuation resume] // register tab and escape to resume (and register escape to pause and tab to history when resuming lol) (let [_resume ->cc { (pauseMenu.hide) (sh.registerItem "{tab} show history" ->cc (showDialogHistory flxMovie.dialogHistory cc) true) (sh.registerItem "{escape} pause" ->cc (showPauseMenu cc) true) (resume) }] (set pauseMenu (new SimpleWindow "HISTORY" FlxColor.BLACK FlxColor.WHITE 0.6 0.9 true null null null "up" "down" ->:Void (_resume null))) (pauseMenu.enableVerticalScrolling) (pauseMenu.enableGamepadInput true) (pauseMenu.makeText "") (doFor element history (case element ((Sound caption) (pauseMenu.makeText "<${caption}>") (pauseMenu.makeText "")) ((Dialog speaker type _wryly text) (pauseMenu.makeText (specialTypesSpeakerName speaker type)) (pauseMenu.makeWrappedText (specialHistoryChars text)) (pauseMenu.makeText "")) ((Super text) (pauseMenu.makeText text) (pauseMenu.makeText "")) (never otherwise))) (sh.registerItem "{tab} resume" _resume true) (sh.registerItem "{escape} resume" _resume true) (pauseMenu.show) (pauseMenu.scrollToBottom))) (method loadSet [path] (let [setSprite (new FlxSprite 0 0)] // Load uniquely so we can draw on sets for specific scenes (setSprite.loadGraphic path false 0 0 true))) (method cloneSet [set] (set.clone)) (method :Void showSet [:FlxSprite setSprite :SceneTime time :ScenePerspective perspective :Appearance appearance :FlxCamera camera :Continuation cc] (doFor layer spriteLayers (set layer.cameras [camera])) (case appearance (FirstAppearance (setSprite.setGraphicSize FlxG.width) (when (> setSprite.height FlxG.height) (setSprite.setGraphicSize 0 FlxG.height)) (setSprite.updateHitbox) (setSprite.screenCenter)) (otherwise)) // draw a sky background depending on time of day, int/ext (ifLet [specialSkySprite (dictGet skySprites time)] { (set skySprite (specialSkySprite.clone)) (skySprite.setGraphicSize (Std.int setSprite.width)) (skySprite.updateHitbox) (unless (>= skySprite.height setSprite.height) (skySprite.setGraphicSize 0 (Std.int setSprite.height)) (skySprite.updateHitbox) (let [hOverflow (- skySprite.width setSprite.width) unscaledOverflow (/ hOverflow skySprite.scale.x) half (iHalf unscaledOverflow)] (set skySprite.clipRect (new FlxRect 0 0 (Std.int (- skySprite.frameWidth unscaledOverflow)) skySprite.frameHeight)))) } (let [skyColor (case time ((or Morning Day) DAY_SKY_COLOR) (Evening EVENING_SKY_COLOR) (Night NIGHT_SKY_COLOR) (never null))] (set skySprite (new FlxSprite)) (skySprite.makeGraphic (Std.int setSprite.width) (Std.int setSprite.height) skyColor true))) (set skySprite.x setSprite.x) (set skySprite.y 0) (set skySprite.cameras [camera]) (.add (first spriteLayers) skySprite) (FlxG.cameras.remove flxMovie.uiCamera false) (FlxG.cameras.remove flxMovie.screenCamera false) (FlxG.cameras.add camera) (FlxG.cameras.add flxMovie.screenCamera) (FlxG.cameras.add flxMovie.uiCamera) (set setSprite.cameras [camera]) (.add (first spriteLayers) setSprite) (cc)) (method :Void hideSet [:FlxSprite set :FlxCamera camera :Continuation cc] (when (FlxG.cameras.list.contains camera) (FlxG.cameras.remove camera false)) (.remove (first spriteLayers) skySprite true) (.remove (first spriteLayers) set true) (cc)) (method :FlxSprite drawLight [:FlxLightSource source] (lightSprite.drawPolygon source.points source.color (object color FlxColor.TRANSPARENT)) (lightMask.drawPolygon source.points FlxColor.BLACK (object color FlxColor.TRANSPARENT))) (var DAY_SKY_COLOR FlxColor.CYAN) (var NIGHT_SKY_COLOR FlxColor.BLACK) (var EVENING_SKY_COLOR (FlxColor.fromRGB 23 28 70)) (var EVENING_COLOR (FlxColor.fromRGBFloat 0.5 0 0.5 0.2)) (var NIGHT_COLOR (FlxColor.fromRGBFloat 0 0 0 0.5)) (prop &mut :FlxSprite lightSprite null) (prop &mut :FlxSprite lightMask null) (prop &mut :FlxSprite darkness null) (prop &mut :FlxColor darkColor FlxColor.BLACK) (var &mut :FlxMouseEventManager mm null) (method :Void chooseString [:String prompt :Array choices :String->Void submit] (_chooseString prompt choices submit)) (method :Void _chooseString [:String prompt :Array choices :String->Void submit &opt :Bool xButton :String xKey] (set pauseMenu (kiss_flixel.SimpleWindow.promptForChoice prompt choices submit FlxColor.BLACK FlxColor.WHITE 0.8 0.8 ?xButton (or xKey "") "left" "right" "up" "down" "enter" null false true)) (pauseMenu.enableGamepadInput true [=>START ""])) (method :Void enterString [:String prompt :String->Void submit] (set pauseMenu (SimpleWindow.promptForString prompt submit))) (method :Void defineFlxPoint [:FlxPoint->Void submit] (unless mm (set mm (new FlxMouseEventManager))) (FlxG.state.add mm) (let [screen (new FlxSprite)] (screen.makeGraphic FlxG.width FlxG.height (FlxColor.fromRGBFloat 0 1 0 0.2)) (set screen.cameras [flxMovie.uiCamera]) (flxMovie.nextFrameActions.push ->:Void { (doFor camera FlxG.cameras.list (set camera.zoom 0.5)) (set flxMovie.uiCamera.zoom 1) (mm.add screen ->screen { (mm.remove screen) (FlxG.state.remove screen true) (let [pos (FlxG.mouse.getScreenPosition FlxG.camera)] (doFor camera FlxG.cameras.list (set camera.zoom 1)) (submit pos)) }) (FlxG.state.add screen) }))) (method :Void defineStagePosition [:FlxCamera camera :StagePosition->Void submit &opt :StagePosition oldPos] (let [db (new DebugLayer)] (set db.cameras [camera]) (when oldPos (db.drawCircle oldPos.x oldPos.y 4 FlxColor.YELLOW 2)) (FlxG.state.add db) (defineFlxPoint ->point {(FlxG.state.remove db true) (submit (new StagePosition point.x point.y (or oldPos?.z 5.0)))}))) (method :Void defineLightSource [:FlxLightSource->Void submit] (let [points []] (withFunctions [ (getNextPoint [] (defineFlxPoint ->point { (points.push point) (getNextPoint) })) ] (getNextPoint) (sh.registerItem "{enter} submit light source" (onceLambda [cc] (doFor camera FlxG.cameras.list (set camera.zoom 1)) (when points // TODO allow color choice (submit (new FlxLightSource points FlxColor.TRANSPARENT))) (cc)) true)))) (method :Void showLighting [:SceneTime sceneTime :Array lightSources :FlxCamera camera] (set lightSprite (new FlxSprite)) (lightSprite.makeGraphic FlxG.width FlxG.height FlxColor.TRANSPARENT true) (set lightMask (new FlxSprite)) (lightMask.makeGraphic FlxG.width FlxG.height FlxColor.WHITE true) (set darkness (new FlxSprite)) // TODO handle morning better (set darkColor (case sceneTime (Evening EVENING_COLOR) (Night NIGHT_COLOR) (otherwise FlxColor.TRANSPARENT))) (darkness.makeGraphic FlxG.width FlxG.height darkColor true) (doFor source lightSources (drawLight source)) (blackAlphaMaskFlxSprite darkness lightMask darkness) (set lightSprite.cameras [flxMovie.screenCamera]) (set darkness.alpha darkColor.alphaFloat) (set darkness.cameras [flxMovie.screenCamera]) (FlxG.state.add darkness) (FlxG.state.add lightSprite)) (method :Void hideLighting [] (when darkness (FlxG.state.remove darkness true) (set darkness null)) (when lightSprite (FlxG.state.remove lightSprite true) (set lightSprite null))) (method :Void cleanup [] (hideTitleCard) (hideBlackScreen) (hideDialog) (hideLighting) (when flxMovie.sceneKey (let [scene (dictGet flxMovie.scenes flxMovie.sceneKey)] (hideSet scene.set scene.camera ->:Void {}) (doFor prop scene.props (hideProp prop.prop scene.camera ->:Void {})) (doFor character scene.characters (hideCharacter character scene.camera ->:Void {})))) // Don't remove the sprite layers, which are added in new() // But DO remove everything in them (doFor layer spriteLayers (layer.clear)) // clear out currentSounds and currentVoiceTracks (restoreOriginalVolumes.clear) (whileLet [sound (currentSounds.pop)] (stopSound sound)) (currentSoundVolumes.clear) (whileLet [vt (currentVoiceTracks.pop)] (stopVoiceTrack vt)) ) (method :Option autoZConfig [] (Some (object zPerLayer flxMovie.STAGE_BEHIND_DY frontLayer 0))) (method loadActor [path] // TODO obviously this is wrong: null) (method :Void showCharacter [:Character character :Appearance appearance :FlxCamera camera :Continuation cc] // Why is this necessary? Who knows // (FlxG.state.remove character.actor true) // Do sizing only on first appearance (whenLet [FirstAppearance appearance] // All actors same width, display centered on x (character.actor.setGraphicSize flxMovie.ACTOR_WIDTH) (character.actor.updateHitbox)) (set character.actor.cameras [camera]) (set character.actor.flipX ?!(= character.stageFacing character.actor.defaultFacing)) // For characters whose images are forward-facing portraits, it's wrong to flip them based on facing: (when character.actor.portraitFacingForward (set character.actor.flipX false)) (set character.actor.x (- character.stagePosition.x (/ character.actor.width 2))) (set character.actor.y character.stagePosition.y) // Bump sprites up from the bottom if they're too tall (cond ((flxMovie.presetPositions.exists (character.stagePosition.stringify)) (let [bottom (+ character.actor.y character.actor.height)] (when (> bottom FlxG.height) (-= character.actor.y (- bottom FlxG.height)))) // Display with y adjusted by z: (-= character.actor.y character.stagePosition.z) (let [layer (- LAYER_MAX 1 (Std.int (/ character.stagePosition.z flxMovie.STAGE_BEHIND_DY)))] (.add (nth spriteLayers layer) character.actor))) (true (.add (nth spriteLayers (min (- LAYER_MAX 1) (Std.int character.stagePosition.z))) character.actor))) (cc)) (method :Void hideCharacter [:Character character :FlxCamera camera :Continuation cc] (FlxG.state.remove character.actor true) (doFor layer spriteLayers (layer.remove character.actor true)) (cc)) (prop &mut :Null nextCC) (method onContinue [:FlxActionDigital continueAction] (whenLet [cc nextCC] (set nextCC null) (cc))) (method :Void startWaitForInput [:Continuation cc] (set nextCC cc)) (method :Void stopWaitForInput [:Continuation cc] (when (= nextCC cc) (set nextCC null))) (var TITLE_Y 240) (var TITLE_SIZE 72) (var TITLE_MARGIN 100) (var SUBTITLES_MARGIN 30) (var SUBTITLES_SIZE 48) (prop &mut :FlxSprite titleCard null) (method :Void showTitleCard [:Array text :Continuation cc] (set titleCard (new FlxSprite)) (titleCard.makeGraphic FlxG.width FlxG.height FlxColor.BLACK true) (SpriteTools.writeOnSprite (text.shift) TITLE_SIZE titleCard (object x (Percent 0.5) y (Pixels TITLE_Y))) (localVar &mut subtitleY (+ TITLE_Y TITLE_SIZE TITLE_MARGIN)) (doFor subtitle text (SpriteTools.writeOnSprite subtitle SUBTITLES_SIZE titleCard (object x (Percent 0.5) y (Pixels subtitleY))) (+= subtitleY SUBTITLES_SIZE SUBTITLES_MARGIN)) (FlxG.state.add titleCard) (cc)) (prop &mut :Bool isLoading false) (var LOAD_CALLS_PER_FRAME 2) (var LOAD_CALLS_PER_FRAME_SCAVENGE 20) (method :Void doLoading [:ArrayVoid> _load :Bool scavenged :Continuation cc] (set isLoading true) (set FlxG.autoPause false) (set FlxG.mouse.visible false) (localVar &mut :flixel.addons.util.FlxAsyncLoop loop null) (let [doneFuncs [] bar (new FlxBar 0 0 LEFT_TO_RIGHT (iThird FlxG.width) SimpleWindow.textSize doneFuncs "length" 0 _load.length true)] (set loop (new flixel.addons.util.FlxAsyncLoop (+ 1 _load.length) ->:Void (ifLet [nextLoad (_load.shift)] { (nextLoad) (doneFuncs.push nextLoad) } { (set FlxG.mouse.visible true) (set isLoading false) (unless (Reflect.field FlxG.state "focused") (pause)) (set FlxG.autoPause true) (FlxG.state.remove bar true) (FlxG.state.remove loop true) (print (/ (/ flash.system.System.totalMemory 1024) 1000) "Memory in use: ") (cc) }) (if scavenged LOAD_CALLS_PER_FRAME_SCAVENGE LOAD_CALLS_PER_FRAME))) (haxe.Timer.delay ->:Void (loop.start) 1) // (set bar.cameras [flxMovie.uiCamera]) (bar.createColoredEmptyBar FlxColor.BLACK true FlxColor.WHITE) (bar.createColoredFilledBar FlxColor.WHITE false) (bar.screenCenter) (FlxG.state.add bar) (FlxG.state.add loop))) (method :Void hideTitleCard [] (when titleCard (FlxG.state.remove titleCard true) (set titleCard null))) // TODO these could be customizable to the Actor, wrylies, etc. (var DIALOG_BOX_COLOR FlxColor.BLACK) (var DIALOG_COLOR FlxColor.WHITE) (var DIALOG_SIZE 24) (prop &mut :FlxSprite dialogBox) (prop &mut :FlxSprite superText) (prop &mut :FlxText dialogText) (prop &mut :FlxText _speakerNameText) (prop :FlxText speakerNameText (property get set)) (method set_speakerNameText [text] (when _speakerNameText (FlxG.state.remove _speakerNameText true) (set _speakerNameText null)) (set _speakerNameText text)) (method get_speakerNameText [] _speakerNameText) (var SUPER_MARGIN 10) (method applyFormat [:FlxText text] (text.applyMarkup text.text [ (new FlxTextFormatMarkerPair (new FlxTextFormat FlxColor.CYAN) "*") ])) (method showDialog [:String speakerName :SpeechType type :String wryly :String text :Continuation cc] // TODO handle text messages, wrylies, off-screen, from-phone, etc. via (case type) // TODO attribute on-screen dialogue to the character's stageposition // TODO allow sounds for wrylies, like the dispatch click (localVar &mut :Float speakerNameX -1) (case type (Super (when superText (FlxG.state.remove superText true)) (set superText (SpriteTools.textPlate text DIALOG_SIZE SUPER_MARGIN null null applyFormat)) // If supertext can be done in a single centered line, do so (when (<= superText.width FlxG.width) (set superText.cameras [flxMovie.uiCamera]) (superText.screenCenter) (set superText.y flxMovie.DIALOG_Y) (FlxG.state.add superText) (startWaitForInput cc) (return))) ((OnScreen character) (set speakerNameX (+ character.actor.x (fHalf character.actor.width))) // When an actor is associated with the line, check for an animation matching the wryly (let [actor (the ActorFlxSprite character.actor)] (if wryly (actor.playAnimation wryly) (actor.playAnimation "neutral")))) ((or (OffScreen actor) (VoiceOver actor) (TextMessage actor) (FromPhone actor)) (if wryly (actor.playAnimation wryly) (actor.playAnimation "neutral"))) (otherwise)) // Make a dialog box (unless dialogBox (set dialogBox (new FlxSprite flxMovie.DIALOG_X flxMovie.DIALOG_Y)) (dialogBox.makeGraphic flxMovie.DIALOG_WIDTH flxMovie.DIALOG_HEIGHT DIALOG_BOX_COLOR)) (set dialogBox.cameras [flxMovie.uiCamera]) (FlxG.state.add dialogBox) (dialogBox.revive) // show the dialog (unless dialogText (set dialogText (new FlxText flxMovie.DIALOG_X flxMovie.DIALOG_Y flxMovie.DIALOG_WIDTH "" DIALOG_SIZE))) (set dialogText.cameras [flxMovie.uiCamera]) (FlxG.state.add dialogText) // *cyan* for italics (set dialogText.text text) (applyFormat dialogText) (set dialogText.size DIALOG_SIZE) (while (< FlxG.height (+ dialogText.y dialogText.height)) (-= dialogText.size 6)) // show the speaker name (set speakerNameText null) (set speakerNameText (new FlxText flxMovie.DIALOG_X flxMovie.DIALOG_Y 0 "" DIALOG_SIZE)) (set speakerNameText.cameras [flxMovie.uiCamera]) (FlxG.state.add speakerNameText) (if speakerName { (set speakerNameText.text "${speakerName}:") (when (= speakerNameX -1) (set speakerNameX flxMovie.DIALOG_X)) (-= speakerNameX (fHalf speakerNameText.width)) (clamp speakerNameX flxMovie.DIALOG_X (- (+ flxMovie.DIALOG_X flxMovie.DIALOG_WIDTH) speakerNameText.width)) (set speakerNameText.x speakerNameX) (speakerNameText.revive) (set dialogText.y (+ flxMovie.DIALOG_Y speakerNameText.height)) } (set dialogText.y flxMovie.DIALOG_Y)) (dialogText.revive) (startWaitForInput cc)) (method :Void hideDialog [] (dialogText?.kill) (speakerNameText?.kill) (dialogBox?.kill) (when superText (FlxG.state.remove superText true))) (savedVar :Float soundVolume 1.0) (savedVar :Float masterVolume 1.0) (prop :Array currentSounds []) (prop :Map> currentSoundVolumes (new Map)) (method loadSound [path] (let [s (FlxG.sound.load path)] (set s.persist true) s)) (method :Void playSound [:FlxSound sound :Float volumeMod &opt :Continuation cc] (let [originalVolume sound.volume onComplete ->{ (currentSounds.remove sound) (currentSoundVolumes.remove sound) (set sound.volume originalVolume) (when cc (cc)) }] (dictSet currentSoundVolumes sound [originalVolume volumeMod]) (*= sound.volume volumeMod soundVolume) (set sound.onComplete onComplete)) (currentSounds.push sound) (sound.play)) (method :Void stopSound [:FlxSound sound] (currentSounds.remove sound) (sound.stop)) (var CAPTION_Y 50) // This group is just for tracking y gaps where a new caption could appear (prop &mut :FlxGroup captions null) (prop :Map captionIds (new Map)) (method :Void showCaption [:String description :Int id] (unless captions (set captions (new FlxGroup))) (let [firstNull (captions.getFirstNull) row (case firstNull (-1 0) (otherwise firstNull)) plate (SpriteTools.textPlate "[${description}]" DIALOG_SIZE SUPER_MARGIN)] (plate.screenCenter) (set plate.y (+ CAPTION_Y (* plate.height row))) (captions.add plate) (dictSet captionIds id plate) (FlxG.state.add plate))) (method :Void hideCaption [:Int id] (let [plate (dictGet captionIds id)] (captions.remove plate) (FlxG.state.remove plate true) (captionIds.remove id))) (method :Float getSoundLength [:FlxSound sound] sound.length) (method loadSong [path] (let [song (FlxG.sound.load path)] (assert !(= 0 song.length) "song from $path has 0 length! avoid mp3s for this reason") song)) (method :Float getSongLength [:FlxSound song] (/ song.length 1000)) (savedVar :Float voiceVolume 1.0) (prop :Array currentVoiceTracks []) (prop :Map restoreOriginalVolumes (new Map)) (method loadVoiceTrack [wavPath] (FlxG.sound.load wavPath)) (method :Void playVoiceTrack [:FlxSound track :Float volumeMod :Float start :Float end :Continuation cc] (let [originalVolume track.volume restoreOriginalVolume ->{(set track.volume originalVolume)(currentSoundVolumes.remove track)}] (dictSet currentSoundVolumes track [originalVolume volumeMod]) (dictSet restoreOriginalVolumes track restoreOriginalVolume) (*= track.volume volumeMod voiceVolume) (set track.onComplete ->{ (currentVoiceTracks.remove track) (restoreOriginalVolume) (cc) })) (currentVoiceTracks.push track) (track.play true (* 1000 start) (* 1000 end))) (method :Void stopVoiceTrack [:FlxSound track] (currentVoiceTracks.remove track) (track.stop) (when (restoreOriginalVolumes.exists track) (dictGet restoreOriginalVolumes track))) (prop &mut :FlxSound music) (prop &mut :FlxTimer musicFadeTimer) (prop MUSIC_FADE_SEC 1) (prop MUSIC_FADE_STEPS 10) (savedVar :Float musicVolume 1.0) // You should normally never call this yourself (method :Void updateMusicVolume [] (when music (let [originalVolumeSet (dictGet currentSoundVolumes music)] (when (> 1.0 (first originalVolumeSet)) (+= (first originalVolumeSet) (/ 1.0 MUSIC_FADE_STEPS))) (set music.volume (* (first originalVolumeSet) (second originalVolumeSet) musicVolume))))) (method :Void playSong [:FlxSound song :Float volumeMod :Bool loop :Bool waitForEnd :Continuation cc] (when music (stopSong)) (let [onFinish ->{ (currentSoundVolumes.remove music) (set music null) (when waitForEnd (cc)) }] (set music (FlxG.sound.play (Reflect.field song "_sound") 0 loop null true onFinish)) // Because music needs to fade in, an array stores its current fade percentage AND its eventual volumeMod target. // All of this is modulated to the musicVolume property which represent's the player's music volume setting (dictSet currentSoundVolumes music [0 volumeMod]) (set musicFadeTimer (.start (new FlxTimer) (/ MUSIC_FADE_SEC MUSIC_FADE_STEPS) ->:Void _ (updateMusicVolume) MUSIC_FADE_STEPS)) (set music.persist true) (unless waitForEnd (cc)))) (method :Void changeSongVolume [:Float volumeMod :Continuation cc] (set music.volume (* volumeMod musicVolume)) (cc)) (method :Void stopSong [] (when music (music.stop)) (when musicFadeTimer (musicFadeTimer.cancel)) (set music null)) (var PROP_MIN_WIDTH 200) (var PROP_MAX_WIDTH 500) (prop &mut :Bool autoPlaceProps true) (method scaleProp [:FlxSprite prop] (let [propKey (_propKey prop)] (unless (flxMovie.propScales.exists propKey) (unless (Std.isOfType prop FlxText) // Set the initial scale so the thing fits on the screen, at least (prop.setGraphicSize FlxG.width) (prop.updateHitbox) (when (> prop.height FlxG.height) (prop.setGraphicSize 0 FlxG.height)) (flxMovie.propScales.put propKey (new HFloat prop.scale.x)))) (let [:Float scale .value (flxMovie.propScales.get propKey)] (when (StringTools.contains propKey "anonProp") (return)) (unless (flxMovie.propsInScene.exists flxMovie.sceneKey) (dictSet flxMovie.propsInScene flxMovie.sceneKey [])) (unless !(= -1 (.indexOf (dictGet flxMovie.propsInScene flxMovie.sceneKey) propKey)) (.push (dictGet flxMovie.propsInScene flxMovie.sceneKey) propKey)) (prop.scale.set scale scale) (prop.updateHitbox)))) (method loadProp [path] (let [propSprite (new FlxSprite 0 0)] // Load props uniquely because they can be drawn on (propSprite.loadGraphic path false 0 0 true))) (method :Void showProp [:FlxSprite prop :StagePosition position :Appearance appearance :FlxCamera camera :Continuation cc] (set prop.cameras [camera]) (let [width (min (max prop.width PROP_MIN_WIDTH) PROP_MAX_WIDTH)] (whenLet [FirstAppearance appearance] (prop.setGraphicSize width) (prop.updateHitbox)) (set prop.x position.x) (set prop.y position.y) // When using preset positions, automatically move everything around and scale it (cond ((flxMovie.presetPositions.exists (position.stringify)) // if the prop is too tall, shrink it heightwise (when (> prop.height flxMovie.DIALOG_Y) (prop.setGraphicSize 0 (Std.int flxMovie.DIALOG_Y)) (prop.updateHitbox)) (-= prop.x (/ prop.width 2)) (-= prop.y (/ prop.height 2)) (let [propBottom (+ prop.y prop.height)] // If a prop goes below the edge, bring it up (when (> propBottom FlxG.height) (-= prop.y (- propBottom FlxG.height))))) // Otherwise, let the director scale props (true (scaleProp prop))) (let [layerNum position.z] (assertEquals layerNum (Std.int layerNum)) (.add (nth spriteLayers (min LAYER_MAX (+ 1 (Std.int layerNum)))) prop))) (cc)) (var :Map _propKeys (new Map)) (var &mut anonProps 0) (function _propKey [:FlxSprite prop] (ifLet [key (dictGet _propKeys prop)] key (dictSet _propKeys prop (or prop.graphic.assetsKey "anonProp#${anonProps++}")))) (method :Void hideProp [:FlxSprite prop :FlxCamera camera cc] (FlxG.state.remove prop true) (unless (flxMovie.propsInScene.exists flxMovie.sceneKey) (dictSet flxMovie.propsInScene flxMovie.sceneKey [])) (let [propKey (_propKey prop)] (.remove (dictGet flxMovie.propsInScene flxMovie.sceneKey) propKey)) (doFor layer spriteLayers (layer.remove prop true)) (cc)) (prop :kiss.List creditsText []) (method _ctext [:String text :Int size] (let [t (new FlxText 0 0 0 text size)] (creditsText.push t) t)) (prop &mut :FlxSprite blackBG null) (method :Void showBlackScreen [] (set blackBG (new FlxSprite)) (blackBG.makeGraphic FlxG.width FlxG.height FlxColor.BLACK true) (FlxG.state.add blackBG)) (method :Void hideBlackScreen [] (when blackBG (FlxG.state.remove blackBG true) (set blackBG null))) (var IDEAL_SCROLL_SPEED 200) // TODO currently the bg will cover whatever the final scene was - making after credits scenes impossible (method :Void rollCredits [:Array credits cc &opt :Float timeLimit] (localVar bg (new FlxSprite)) (bg.makeGraphic FlxG.width FlxG.height FlxColor.BLACK true) (set bg.cameras [flxMovie.uiCamera]) (FlxG.state.add bg) (localVar &mut textY FlxG.height) (var oneColSize 64) (var twoColSize 48) (var threeColSize 32) (var creditMargin 20) (var twoColumnGap 50) (doFor line credits (case line (Break (+= textY oneColSize)) // Centered, big one column lines ((OneColumn col1) (let [t (_ctext col1 oneColSize)] (t.screenCenter) (set t.y textY)) (+= textY oneColSize creditMargin)) // Centered left/right two column lines ((TwoColumn col1 col2) (let [t1 (_ctext col1 twoColSize) t2 (_ctext col2 twoColSize)] (set t1.x (- (* FlxG.width 0.3) t1.width (/ twoColumnGap 2))) (set t1.y textY) (set t2.x (+ (* FlxG.width 0.3) (/ twoColumnGap 2))) (let [clippedRight (- (+ t2.x t2.width) FlxG.width)] (when (> clippedRight 0) (-= t2.x clippedRight))) (set t2.y textY)) (+= textY twoColSize creditMargin)) // Left-justified, centered, right-justified three column lines ((ThreeColumn col1 col2 col3) (let [t1 (_ctext col1 threeColSize) t2 (_ctext col2 threeColSize) t3 (_ctext col3 threeColSize)] (set t1.x creditMargin) (set t1.y textY) (t2.screenCenter) (set t2.y textY) (set t3.x (- FlxG.width creditMargin t3.width)) (set t3.y textY)) (+= textY threeColSize creditMargin)) (otherwise))) (let [pixelsToScroll (+ textY .height (last creditsText)) idealTimeLimit (/ pixelsToScroll IDEAL_SCROLL_SPEED) timeLimitDifference (- idealTimeLimit timeLimit) scrollSpeed (if timeLimit (/ pixelsToScroll timeLimit) IDEAL_SCROLL_SPEED)] (#when debug (when !(= scrollSpeed IDEAL_SCROLL_SPEED) (print "Time given for credit roll (${timeLimit}s) is $(Math.abs timeLimitDifference)sec $(if (> timeLimitDifference 0) "too short" "too long") for ideal speed."))) (doFor text creditsText (FlxG.state.add text) (set text.cameras [flxMovie.uiCamera]) (let [:Array tweenArgs []] (tweenArgs.push false) (tweenArgs.push text) (tweenArgs.push text.x) (tweenArgs.push (- text.y textY)) (tweenArgs.push scrollSpeed) (tweenArgs.push cc) (Reflect.callMethod flxMovie (Reflect.field flxMovie "linearMotion") tweenArgs))))) (prop &mut :FlxSprite inputIcon) (method :Void showInputIcon [] (set inputIcon (SpriteTools.textPlate "[...]" DIALOG_SIZE SUPER_MARGIN null null applyFormat)) (set inputIcon.cameras [flxMovie.uiCamera]) (inputIcon.screenCenter) (set inputIcon.y (- flxMovie.DIALOG_Y inputIcon.height)) (FlxG.state.add inputIcon)) (method :Void hideInputIcon [] (FlxG.state.remove inputIcon true))