Files
hollywoo-flixel/src/hollywoo_flixel/FlxDirector.kiss

1000 lines
41 KiB
Plaintext

(prop :FlxActionDigital continueAction)
(prop :FlxActionDigital fastForwardAction)
(prop actionManager (new FlxActionManager))
(prop &mut :Movie<FlxSprite,ActorFlxSprite,FlxSound,FlxSound,FlxSprite,FlxSound,FlxCamera,FlxLightSource> movie)
(defAlias &ident flxMovie (cast movie FlxMovie))
(loadFrom "hollywoo-flixel" "src/hollywoo_flixel/Aliases.kiss")
(prop :Array<FlxTypedGroup<FlxSprite>> spriteLayers [])
(var LAYER_MAX 8)
(savedVar :Map<String,String> 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<FlxSprite>)]
(spriteLayers.push g)
(FlxG.state.add g)))))
(prop :Map<SceneTime,FlxSprite> skySprites (new Map))
(prop &mut :FlxSprite skySprite)
(prop :FlxKeyShortcutHandler<Continuation->Void> sh (new FlxKeyShortcutHandler))
(method :KeyShortcutHandler<Continuation->Void> 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<FlxSound> 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 "✓" "<check mark>"))
(function specialTypesSpeakerName [:String name :SpeechType<ActorFlxSprite> type]
(case type
((VoiceOver _)
"${name} (voiceover)")
((TextMessage _)
"${name} (text message)")
((FromPhone _)
"${name} (from phone)")
((Custom type _ _)
"${name} (${type})")
(otherwise name)))
(method :Void showDialogHistory [:Array<HistoryElement<ActorFlxSprite>> 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<String> choices :String->Void submit]
(_chooseString prompt choices submit))
(method :Void _chooseString [:String prompt :Array<String> 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<FlxLightSource> 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> autoZConfig [] (Some (object zPerLayer flxMovie.STAGE_BEHIND_DY frontLayer 0)))
(method loadActor [path]
// TODO obviously this is wrong:
null)
(method :Void showCharacter [:Character<ActorFlxSprite> 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<ActorFlxSprite> character :FlxCamera camera :Continuation cc]
(FlxG.state.remove character.actor true)
(doFor layer spriteLayers
(layer.remove character.actor true))
(cc))
(prop &mut :Null<Continuation> 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<String> 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 [:Array<Void->Void> _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<ActorFlxSprite> 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<FlxSound> currentSounds [])
(prop :Map<FlxSound,Array<Float>> 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<Int,FlxSprite> 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<FlxSound> currentVoiceTracks [])
(prop :Map<FlxSound,Function> 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<FlxSprite,String> _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<FlxText> 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<CreditsLine> 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<Dynamic> 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))