diff --git a/assets/config/assets.onj b/assets/config/assets.onj index 12db10fc8..faef4fe44 100644 --- a/assets/config/assets.onj +++ b/assets/config/assets.onj @@ -1212,6 +1212,12 @@ shaders: [ constantArgs: { } }, + { + name: "warp_shader", + file: "shaders/warp.glsl", + constantArgs: { + } + }, { name: "slider_shader", file: "shaders/slider.glsl", diff --git a/assets/config/cards.onj b/assets/config/cards.onj index ba67bcc84..8a617f3fa 100644 --- a/assets/config/cards.onj +++ b/assets/config/cards.onj @@ -1515,6 +1515,25 @@ cards: [ tags: ["pool1", "rarity2"], price: 50, }, + { + name: "wardenOfTimeBullet", + title: "Warden Of Time Bullet", + flavourText: "", + description: "3 revolver rotations after that this gets shot: destroy the bullet in slot 5 " + slot.5 + " and this bullet gets put there instead. If there is no bullet, nothing gets destroyed and this Bullet just appears.", + baseDamage: 4, + coverValue: 0, + traitEffects: [ "wardenOfTime" ], + rotation: $Right { amount: 1 }, + highlightType: "standard", + effects: [ + ], + forceLoadCards: [], + dark: true, + cost: 2, + type: $Bullet { }, + tags: ["pool2", "rarity1"], + price: 50, + }, ], diff --git a/assets/screens/game_screen.onj b/assets/screens/game_screen.onj index 55278c715..a14f187ca 100644 --- a/assets/screens/game_screen.onj +++ b/assets/screens/game_screen.onj @@ -65,7 +65,7 @@ assets: { "holster_sound", "money_sound", "revolver_drum_sound", "parry_sound", "swoosh_sound", "button_2_sound", "card_destroy_sound", "holster_remove_sound", "metal_on_metal_sound", - "screen_shake_popout_shader", + "screen_shake_popout_shader", "warp_shader", "map_theme", "encounter_theme", diff --git a/assets/screens/title_screen.onj b/assets/screens/title_screen.onj index 144ce4400..7ba0401af 100644 --- a/assets/screens/title_screen.onj +++ b/assets/screens/title_screen.onj @@ -34,6 +34,7 @@ assets: { "common_button_default", "common_button_hover", "main_theme", + "warp_shader", "swoosh_sound", "button_2_sound", ] }, @@ -97,7 +98,6 @@ var confirmationPopup = $Box { ], touchable: "enabled" } children [ - $Box { styles: [ { diff --git a/assets/shaders/warp.glsl b/assets/shaders/warp.glsl new file mode 100644 index 000000000..4dd4c2a26 --- /dev/null +++ b/assets/shaders/warp.glsl @@ -0,0 +1,123 @@ +~~~section vertex + +%include shaders/includes/default_vertex.glsl + +~~~section fragment + +#ifdef GL_ES +#define LOWP lowp +precision mediump float; +#else +#define LOWP +#endif + +varying LOWP vec4 v_color; +varying vec2 v_texCoords; +uniform sampler2D u_texture; + +%uniform u_time +uniform float u_progress;// between 0-1 +uniform vec2 u_center;// between 0-1 for x and y +uniform float u_depth;// recommended beween 3 and 30, how strong it zooms +uniform float u_rotation;// rotation in radians + +// "streches" a value, fe. oldDistance 0.1=>0.23; 0.9=>0.96 (examplevalues with depth=2) +float getNewDist(float oldDist, float depthMulti){ + float depth=15.0*depthMulti; + if (u_depth>1.0){ + depth=u_depth*depthMulti; + } + float a = pow(2.0, depth); + a=a/(a-1.0); + return (a-a/pow(oldDist+1.0, depth));//from 0-1 returns 0-1 but "warped" +} + +float hypo(vec2 oldDist){ + return sqrt(oldDist.x*oldDist.x+oldDist.y*oldDist.y); + // return abs(oldDist.x)+abs(oldDist.y); +} + +//the position where the extended line between the center and the pixel hits the (0|0),(0|1),(1|1),(1|0) square +vec2 getBorderIntersection(float k, vec2 pointOnLine, bool rightOfCenter){ + + float d = pointOnLine.y - pointOnLine.x * k; + vec2 borderIntersection; + if (rightOfCenter) borderIntersection= vec2(1.0, k+d); + else borderIntersection=vec2(0.0, d); + + if (borderIntersection.y>1.0) borderIntersection=vec2((1.0-d) / k, 1.0); + else if (borderIntersection.y<0.0) borderIntersection=vec2(-d/k, 0.0); + return borderIntersection; +} + +// rotates the slope, and the isRightOfCenter says if the direction is (oldSlope,1) if true or (-oldslope,-1) if false +vec2 rotate(float oldSlope, float rotation, bool isRightOfCenter){ + float PI=radians(180.0); + float oldRotation=atan(oldSlope, 1.0); + if(!isRightOfCenter) oldRotation+=PI; + float newRotation=oldRotation+rotation; + + if(cos(newRotation)>0.0) return vec2(tan(newRotation),1.0); + else return vec2(tan(newRotation),0.0); +} + +// This programm takes the following steps: +// 1. calculate the line between the current position and the center +// 2. get the point on the line where it hits the min/max box (Rectangle with coordiantes: (0|0),(0|1),(1|1),(1|0)) +// 3. calculate the distance from in percent between the center and that point for my position +// 4. strech this position, so that the closer you are to the center, the stronger it streches and goes away from it +// 5. then it rotates the line from 1 +// 6. repeat step 2 with the new line +// 7. takes the percentages from point 3 and puts them on the line from point 6 + +void main() { + +// float progress = (sin(abs(float(u_time*0.85)))+1.0)/2.0; + float progress = u_progress; + + vec2 center = u_center; + if(u_center.x==0.0 && u_center.y==0.0) center=vec2(0.5, 0.8); + +// float PI = radians(180.0); +// float rotation = getNewDist(progress, -0.1) * PI/2.0*16.0; + float rotation = getNewDist(progress, -0.1) * u_rotation; + + + vec2 tc = v_texCoords; + vec2 distToCenter= tc-center; + + // 1. + float k = distToCenter.y/distToCenter.x; // slope of line from + + bool pointRightOfCenter=tc.x>center.x; + + // 2. + vec2 borderIntersection= getBorderIntersection(k, center, pointRightOfCenter); + + + // 3. + float maxDist=hypo(borderIntersection-center); + + + float maxDistToFurthestCorner=max(max(hypo(center),hypo(center-vec2(0.0,1.0))), max(hypo(center-vec2(1.0)),hypo(center-vec2(1.0,0.0)))); + float outSideMultiplier=maxDist/maxDistToFurthestCorner; //this makes it from a rectangle to a ellipse + float oldPercent=hypo(distToCenter)/maxDist; + + // 4. + float strechedPercent=getNewDist(oldPercent, outSideMultiplier); + + + //5. + vec2 newRot = rotate(k, rotation*(1.0-strechedPercent), pointRightOfCenter); //newRot[0]= new slope, newRot[1] == 1 if new point is right of center + + + //6 + vec2 rotatedBorderToCenter = getBorderIntersection(newRot.x, center, newRot.y==1.0) - center; + + + //7 + vec2 rotatedPos = center+rotatedBorderToCenter*oldPercent; + vec4 result = texture2D(u_texture, rotatedPos + (strechedPercent-oldPercent) * rotatedBorderToCenter * progress); + + gl_FragColor = result; +} \ No newline at end of file diff --git a/core/src/com/fourinachamber/fortyfive/game/GameController.kt b/core/src/com/fourinachamber/fortyfive/game/GameController.kt index 330201baa..f46c77f99 100644 --- a/core/src/com/fourinachamber/fortyfive/game/GameController.kt +++ b/core/src/com/fourinachamber/fortyfive/game/GameController.kt @@ -139,6 +139,10 @@ class GameController(onj: OnjNamedObject) : ScreenController() { private var selectedCard: Card? = null + private val _limbo: MutableList = mutableListOf() + val limbo: List + get() = _limbo + /** * counts up every turn; starts at 0, but gets immediately incremented to one */ @@ -368,6 +372,13 @@ class GameController(onj: OnjNamedObject) : ScreenController() { updateStatusEffects() updateGameAnimations() updateTutorialText() + val limboTimeline = _limbo.mapNotNull { it.updateInLimbo(this) } + if (limboTimeline.isEmpty()) return + appendMainTimeline(limboTimeline.collectTimeline()) + } + + fun removeFromLimbo(card: Card) { + _limbo.remove(card) } private fun updateTutorialText() { @@ -553,6 +564,30 @@ class GameController(onj: OnjNamedObject) : ScreenController() { ) }) + fun placeBulletInRevolverDirect(card: Card, slot: Int): Timeline = Timeline.timeline { + val triggerInfo = TriggerInformation(sourceCard = card, controller = this@GameController) + action { + revolver.setCard(slot, card) + card.onEnter(this@GameController) + } + includeLater( + { + encounterModifiers + .mapNotNull { it.executeAfterBulletWasPlacedInRevolver(card, this@GameController) } + .collectTimeline() + }, + { true } + ) + includeLater( + { checkEffectsSingleCard(Trigger.ON_ENTER, card, triggerInfo) }, + { true } + ) + includeLater( + { checkEffectsActiveCards(Trigger.ON_ANY_CARD_ENTER, triggerInfo) }, + { true } + ) + } + fun putCardFromRevolverBackInHand(card: Card) { FortyFiveLogger.debug(logTag, "returning card $card from the revolver to the hand") revolver.removeCard(card) @@ -707,13 +742,15 @@ class GameController(onj: OnjNamedObject) : ScreenController() { curScreen.leaveState(showEnemyAttackPopupScreenState) gameRenderPipeline.stopParryEffect() if (parryCard.shouldRemoveAfterShot(this@GameController)) { - if (!parryCard.isUndead) { + if (!parryCard.isUndead && !parryCard.isWardenOfTime) { SoundPlayer.situation("orb_anim_playing", curScreen) gameRenderPipeline.addOrbAnimation(cardOrbAnim(parryCard.actor)) } revolver.removeCard(parryCard) if (parryCard.isUndead) { cardHand.addCard(parryCard) + } else if (parryCard.isWardenOfTime) { + _limbo.add(parryCard) } else { putCardAtBottomOfStack(parryCard) } @@ -861,7 +898,7 @@ class GameController(onj: OnjNamedObject) : ScreenController() { ) action { if (cardToShoot.shouldRemoveAfterShot(this@GameController)) { - if (!cardToShoot.isUndead) { + if (!cardToShoot.isUndead && !cardToShoot.isWardenOfTime) { putCardAtBottomOfStack(cardToShoot) SoundPlayer.situation("orb_anim_playing", curScreen) gameRenderPipeline.addOrbAnimation(cardOrbAnim(cardToShoot.actor)) @@ -869,6 +906,7 @@ class GameController(onj: OnjNamedObject) : ScreenController() { revolver.removeCard(cardToShoot) } if (cardToShoot.isUndead) cardHand.addCard(cardToShoot) + if (cardToShoot.isWardenOfTime) _limbo.add(cardToShoot) cardToShoot.afterShot(this@GameController) } } diff --git a/core/src/com/fourinachamber/fortyfive/game/card/Card.kt b/core/src/com/fourinachamber/fortyfive/game/card/Card.kt index 140db8185..251cbed6f 100644 --- a/core/src/com/fourinachamber/fortyfive/game/card/Card.kt +++ b/core/src/com/fourinachamber/fortyfive/game/card/Card.kt @@ -153,6 +153,8 @@ class Card( private set var isAlwaysAtTop: Boolean = false private set + var isWardenOfTime: Boolean = false + private set fun shouldRemoveAfterShot(controller: GameController): Boolean = !( (isEverlasting && !controller.encounterModifiers.any { it.disableEverlasting() }) || @@ -255,6 +257,59 @@ class Card( } } + private var wardenOfTimeRotationCounter: Int = -1 + + fun updateInLimbo(controller: GameController): Timeline? { + if (!isWardenOfTime || wardenOfTimeRotationCounter == -1) return null + if (wardenOfTimeRotationCounter + 4 > controller.revolverRotationCounter) return null + wardenOfTimeRotationCounter = -1 + val growAction = ScaleToAction() + growAction.duration = 0.3f + growAction.setScale(3f) + growAction.interpolation = Interpolation.pow5In + val shrinkAction = ScaleToAction() + shrinkAction.duration = 0.5f + shrinkAction.setScale(1f) + shrinkAction.interpolation = Interpolation.smoother + + return Timeline.timeline { + action { + controller.removeFromLimbo(this@Card) + } + includeLater( + { controller.destroyCardTimeline(controller.revolver.getCardInSlot(5)!!) }, + { controller.revolver.getCardInSlot(5) != null } + ) + parallelActions( + Timeline.timeline { + delay(800) + parallelActions( + controller.placeBulletInRevolverDirect(this@Card, 5).asAction(), + controller.gameRenderPipeline.liftActor(1000, actor).asAction(), + Timeline.timeline { + action { + actor.setScale(0.3f) + actor.addAction(growAction) + } + delayUntil { growAction.isComplete } + action { + actor.removeAction(growAction) + actor.addAction(shrinkAction) + } + delayUntil { shrinkAction.isComplete } + action { actor.removeAction(shrinkAction) } + }.asAction(), + Timeline.timeline { + delay(250) + include(controller.gameRenderPipeline.getScreenShakePopoutTimeline()) + }.asAction() + ) + }.asAction(), + controller.gameRenderPipeline.getTimeWarpTimeline().asAction() + ) + } + } + fun activeModifiers(controller: GameController): List = modifiers.filter { it.activeChecker(controller) } @@ -268,6 +323,7 @@ class Card( * called by gameScreenController when the card was shot */ fun afterShot(controller: GameController) { + wardenOfTimeRotationCounter = controller.revolverRotationCounter if (shouldRemoveAfterShot(controller)) leaveGame() if (protectingModifiers.isNotEmpty()) { val effect = protectingModifiers.first() @@ -621,6 +677,7 @@ class Card( "rotten" -> card.isRotten = true "alwaysAtBottom" -> card.isAlwaysAtBottom = true "alwaysAtTop" -> card.isAlwaysAtTop = true + "wardenOfTime" -> card.isWardenOfTime = true else -> throw RuntimeException("unknown trait effect $effect") } @@ -666,13 +723,16 @@ class CardActor( override val screen: OnjScreen, val enableHoverDetails: Boolean ) : Widget(), ZIndexActor, KeySelectableActor, DisplayDetailsOnHoverActor, HoverStateActor, HasOnjScreen, StyledActor, - OffSettable, AnimationActor { + OffSettable, AnimationActor, LiftableActor { override val actor: Actor = this override var actorTemplate: String = "card_hover_detail" // TODO: fix override var detailActor: Actor? = null + override var inLift: Boolean = false + override var inLiftRender: Boolean = false + override var inAnimation: Boolean = false override var mainHoverDetailActor: String? = "cardHoverDetailMain" @@ -781,6 +841,7 @@ class CardActor( } override fun draw(batch: Batch?, parentAlpha: Float) { + if (!shouldRender) return validate() if (drawPixmapMessage?.isFinished ?: false) { finishPixmapDrawing() diff --git a/core/src/com/fourinachamber/fortyfive/rendering/RenderPipeline.kt b/core/src/com/fourinachamber/fortyfive/rendering/RenderPipeline.kt index fd990a354..17b9249e9 100644 --- a/core/src/com/fourinachamber/fortyfive/rendering/RenderPipeline.kt +++ b/core/src/com/fourinachamber/fortyfive/rendering/RenderPipeline.kt @@ -10,8 +10,8 @@ import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.badlogic.gdx.graphics.glutils.FrameBuffer import com.badlogic.gdx.graphics.glutils.ShapeRenderer import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType -import com.badlogic.gdx.math.CatmullRomSpline -import com.badlogic.gdx.math.Vector2 +import com.badlogic.gdx.math.* +import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.utils.Drawable import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Disposable @@ -20,12 +20,15 @@ import com.badlogic.gdx.utils.TimeUtils import com.badlogic.gdx.utils.viewport.ExtendViewport import com.fourinachamber.fortyfive.game.GraphicsConfig import com.fourinachamber.fortyfive.game.UserPrefs +import com.fourinachamber.fortyfive.game.card.Card import com.fourinachamber.fortyfive.screen.Resource import com.fourinachamber.fortyfive.screen.ResourceHandle import com.fourinachamber.fortyfive.screen.ResourceManager import com.fourinachamber.fortyfive.screen.general.OnjScreen +import com.fourinachamber.fortyfive.screen.general.customActor.LiftableActor import com.fourinachamber.fortyfive.utils.* import java.lang.Long.max +import kotlin.math.abs import kotlin.math.absoluteValue interface Renderable { @@ -74,6 +77,11 @@ open class RenderPipeline( } private val gaussianBlurShader: BetterShader by gaussianBlurShaderDelegate + private val warpShaderDelegate = lazy { + ResourceManager.get(screen, "warp_shader") + } + private val warpShader: BetterShader by warpShaderDelegate + private var orbFinisesAt: Long = -1 private val isOrbAnimActive: Boolean get() = TimeUtils.millis() <= orbFinisesAt @@ -92,6 +100,41 @@ open class RenderPipeline( shaderPostProcessingStep(screenShakePopoutShader) } + private var warpStartTime: Long = 0 + private var warpDuration: Int = 0 + + private val warpPostProcessingStep: () -> Unit = lambda@{ + val time = TimeUtils.millis() + val passedTime = time - warpStartTime + val absoluteProgress = passedTime.toFloat() / warpDuration.toFloat() + val progress = (1f - abs(absoluteProgress - 0.5f) - 0.5f) * 2f + val shader = warpShader + val (_, inactive) = frameBufferManager.getPingPongFrameBuffers("pp") ?: return@lambda + batch.flush() + batch.shader = shader.shader + shader.shader.bind() + shader.prepare(screen) + + var interpolated: Float = progress + interpolated = Interpolation.smoother.apply(interpolated) + interpolated = Interpolation.swing.apply(interpolated) + + shader.shader.setUniformf("u_progress", interpolated) + shader.shader.setUniformf("u_center", Vector2(0.5f, 0.38f)) + shader.shader.setUniformf("u_depth", 2.9f) + shader.shader.setUniformf("u_rotation", 0.19f * interpolated) + batch.enableBlending() + batch.draw( + inactive.colorBufferTexture, + 0f, 0f, + screen.viewport.worldWidth, + screen.viewport.worldHeight, + 0f, 0f, 1f, 1f // flips the y-axis + ) + batch.flush() + batch.shader = null + } + private val fadeToBlackTask: () -> Unit = { val now = TimeUtils.millis() screen.viewport.apply() @@ -112,6 +155,52 @@ open class RenderPipeline( // postPreprocessingSteps.add(shaderPostProcessingStep(ResourceManager.get(screen, "test_shader"))) } + fun liftActor(time: Int, actor: LiftableActor): Timeline = Timeline.timeline { + val task = liftActorTask(actor.actor, actor) + action { + actor.inLift = true + lateTasks.add(task) + } + delay(time) + action { + actor.inLift = false + lateTasks.remove(task) + } + } + + private fun liftActorTask(actor: Actor, liftableActor: LiftableActor): () -> Unit = { + screen.viewport.apply() + shapeRenderer.projectionMatrix = screen.viewport.camera.combined + + val (x, y) = actor.localToStageCoordinates(Vector2(0f, 0f)) + + val oldTransform = batch.transformMatrix.cpy() + val worldTransform = Affine2() + worldTransform.set(oldTransform) + worldTransform.translate(x - actor.x, y - actor.y) + val computed = Matrix4() + computed.set(worldTransform) + batch.transformMatrix = computed + + liftableActor.inLiftRender = true + actor.draw(batch, 1f) + liftableActor.inLiftRender = false + batch.transformMatrix = oldTransform + } + + fun getTimeWarpTimeline(): Timeline = Timeline.timeline { + val duration = 2000 + action { + warpStartTime = TimeUtils.millis() + warpDuration = duration + postPreprocessingSteps.add(warpPostProcessingStep) + } + delay(duration) + action { + postPreprocessingSteps.remove(warpPostProcessingStep) + } + } + fun getFadeToBlackTimeline(fadeDuration: Int, stayBlack: Boolean = false): Timeline = Timeline.timeline { action { this@RenderPipeline.fadeDuration = fadeDuration @@ -386,6 +475,7 @@ open class RenderPipeline( if (alphaReductionShaderDelegate.isInitialized()) alphaReductionShader.dispose() if (screenShakeShaderDelegate.isInitialized()) screenShakeShader.dispose() if (screenShakePopoutShaderDelegate.isInitialized()) screenShakePopoutShader.dispose() + if (warpShaderDelegate.isInitialized()) warpShader.dispose() } data class OrbAnimation( diff --git a/core/src/com/fourinachamber/fortyfive/screen/general/OnjScreen.kt b/core/src/com/fourinachamber/fortyfive/screen/general/OnjScreen.kt index 28b110613..a49142329 100644 --- a/core/src/com/fourinachamber/fortyfive/screen/general/OnjScreen.kt +++ b/core/src/com/fourinachamber/fortyfive/screen/general/OnjScreen.kt @@ -24,6 +24,7 @@ import com.fourinachamber.fortyfive.game.GraphicsConfig import com.fourinachamber.fortyfive.keyInput.KeyInputMap import com.fourinachamber.fortyfive.keyInput.KeySelectionHierarchyBuilder import com.fourinachamber.fortyfive.keyInput.KeySelectionHierarchyNode +import com.fourinachamber.fortyfive.rendering.BetterShader import com.fourinachamber.fortyfive.rendering.Renderable import com.fourinachamber.fortyfive.screen.ResourceBorrower import com.fourinachamber.fortyfive.screen.ResourceHandle @@ -422,6 +423,7 @@ open class OnjScreen @MainThreadOnly constructor( actor.localToStageCoordinates(Vector2(actor.width / 2, actor.height / 2)) } + @MainThreadOnly override fun render(delta: Float) = try { // Thread.sleep(800) //TODO remove // (please don't, its great to find this method) diff --git a/core/src/com/fourinachamber/fortyfive/screen/general/customActor/ActorInterfaces.kt b/core/src/com/fourinachamber/fortyfive/screen/general/customActor/ActorInterfaces.kt index 5a22a1bdc..2c76aed5e 100644 --- a/core/src/com/fourinachamber/fortyfive/screen/general/customActor/ActorInterfaces.kt +++ b/core/src/com/fourinachamber/fortyfive/screen/general/customActor/ActorInterfaces.kt @@ -333,3 +333,23 @@ interface ActorWithAnimationSpawners { inline fun ActorWithAnimationSpawners.findAnimationSpawner(): T? = animationSpawners.find { it is T } as? T + +interface LiftableActor { + + var inLift: Boolean + var inLiftRender: Boolean + + val shouldRender: Boolean + get() = !inLift || (inLift && inLiftRender) + + val actor: Actor + + fun beginLift() { + inLift = true + } + + fun endLift() { + inLift = false + } + +}