// LunarLander.groovy // odelia technologies @Grapes([ @Grab('org.codehaus.groovyfx:groovyfx:0.4.0'), @Grab('org.reactfx:reactfx:2.0-M4'), @Grab('de.jensd:fontawesomefx:8.4') ]) import static groovyx.javafx.GroovyFX.start import javafx.scene.input.MouseEvent import javafx.beans.binding.Bindings import javafx.beans.value.ChangeListener import org.reactfx.EventStreams import org.reactfx.value.Var import java.time.Duration import de.jensd.fx.glyphs.GlyphsDude import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon def FUEL_START_VALUE = 200 def POSITION_MAX_VALUE = 4000d def POSITION_START_VALUE = 2700d def VELOCITY_THRESHOLD = 20 thrust = Var.newSimpleVar(false) fuel = Var.newSimpleVar(FUEL_START_VALUE) position = Var.newSimpleVar(POSITION_START_VALUE) velocity = Var.newSimpleVar(0) pause = Var.newSimpleVar(true) def reinitialize = { thrust.value = false fuel.value = FUEL_START_VALUE position.value = POSITION_START_VALUE velocity.value = 0 pause.value = true lander.with { scaleX = scaleY = 1d opacity = 1d } } start { stage(title: 'LunarLander', visible: true) { scene = scene(width: 800, height: 600) { pane { vbox(spacing: 5) { hbox(spacing: 3) { stackPane { rectangle(width: 100, height: 70, fill: DARKSEAGREEN) vbox(alignment: "center") { text('POSITION', font: 'bold 11pt "Amble"', fill: WHITE) text(text: bind(position).using({"$it"}), font: '11pt "Amble"', fill: WHITE) } } stackPane { rectangle(width: 100, height: 70, fill: bind(velocity).using { velocity.value < -VELOCITY_THRESHOLD ? RED : DARKCYAN } ) vbox(alignment: "center") { text('VELOCITY', font: 'bold 11pt "Amble"', fill: WHITE) text(text: bind(velocity).using({"$it"}), font: '11pt "Amble"', fill: WHITE) } } stackPane { rectangle(width: 100, height: 70, fill: CORAL) text('FUEL', font: 'bold 11pt "Amble"', fill: WHITE) group(translateY: 20d) { rectangle(width: 100, height: 10, fill: CORAL) rectangle(width: bind(fuel).using({100d*it/FUEL_START_VALUE}), height: 10, fill: FIREBRICK) } } stackPane { rectangle(width: 100, height: 70, fill: MAROON) vbox(alignment: "center") { text('THRUST', font: 'bold 11pt "Amble"', fill: WHITE) text(text: bind(thrust).using { thrust.value ? "[ON]" : "[OFF]" }, font: 'bold 10pt "Amble"', fill: WHITE, onMousePressed { thrust.value = !thrust.value }) } } text('GAME PAUSED', font: 'bold 20pt "Amble"', fill: GREY, visible: bind(pause)) } // Buttons hbox(spacing: 5) { button('Play!', onAction: { pause.value = false }) button('Pause', onAction: { pause.value = true }) button('Reinitialize', onAction: reinitialize) } } ground = rectangle(height: 80, fill: DARKSEAGREEN) lander = group { booster = circle(translateX: 14, radius: 5, fill: YELLOW) node(GlyphsDude.createIcon(FontAwesomeIcon.ROCKET, "30px"), rotate: -45d) crashAnimation = parallelTransition { scaleTransition(200.ms, to: 4d) fadeTransition(200.ms, to: 0d) } } } } } // Bindings for the lander lander.translateYProperty().bind Bindings.createDoubleBinding({scene.height-80-position.value*(scene.height)/POSITION_MAX_VALUE}, position) lander.translateXProperty().bind Bindings.createDoubleBinding({scene.width/2-lander.boundsInLocal.width/2}, scene.widthProperty()) // Bindings for the ground ground.widthProperty().bind scene.widthProperty() ground.translateYProperty().bind Bindings.createDoubleBinding({scene.height-80}, scene.heightProperty()) // Binding for displaying lander's booster booster.opacityProperty() bind Bindings.createDoubleBinding({thrust.value && fuel.value > 0d ? 1d : 0d}, thrust, fuel) // touched binding def touched = Bindings.createBooleanBinding({ position.value <= 0 }, position) EventStreams.changesOf(touched).distinct().filter { it.newValue }.subscribe { position.value = 0; if (Math.abs(velocity.value) > VELOCITY_THRESHOLD) crashAnimation.playFromStart() } // The game loop def ticks = EventStreams.ticks(Duration.ofMillis(100)) ticks.suppressWhen(pause).filter { !touched.value }.subscribe { fuel.value -= (thrust.value ? 1 : 0) def acceleration = thrust.value && fuel.value >= 0 ? 2 : -1 velocity.value += acceleration.value position.value += velocity.value } }