src/*/*_serialize.nit
-src/client/drawing.nit
config.json
# See the License for the specific language governing permissions and
# limitations under the License.
-all: bin/server bin/tinks bin/tinks3d
+all: bin/server bin/tinks3d
-pre-build: assets/images/drawing.png src/server/server_serialize.nit
+pre-build: src/server/server_serialize.nit
# Client
-bin/tinks: assets/images/drawing.png src/client/client.nit $(shell nitls -M src/client/linux_client.nit)
- nitserial -o src/client/client_serialize.nit src/client/client.nit
- nitc -o bin/tinks src/client/linux_client.nit -m src/client/client_serialize.nit
-
bin/tinks3d: $(shell nitls -M src/client/client3d.nit -m linux)
nitserial -o src/client/client_serialize.nit src/client/client3d.nit
nitc -o bin/tinks3d src/client/client3d.nit \
-m src/client/client_serialize.nit -m linux
-assets/images/drawing.png: art/drawing.svg
- ../inkscape_tools/bin/svg_to_png_and_nit art/drawing.svg -a assets/ -s src/client/ -x 2.0
-
# Server
bin/server: src/server/server_serialize.nit $(shell nitls -M src/server/dedicated.nit)
nitc -o bin/server src/server/dedicated.nit -m src/server/server_serialize.nit
nitserial -o src/server/server_serialize.nit src/server/dedicated.nit
# Android
-android: bin/tinks.apk
-bin/tinks.apk: assets/images/drawing.png android/res/ $(shell nitls -M src/client/android_client.nit)
- nitserial -o src/client/client_serialize.nit src/client/client.nit
- nitc -o bin/tinks.apk src/client/android_client.nit -m src/client/client_serialize.nit
-
-android-release: assets/images/drawing.png android/res/ $(shell nitls -M src/client/android_client.nit)
- nitserial -o src/client/client_serialize.nit src/client/client.nit
- nitc -o bin/tinks.apk src/client/android_client.nit -m src/client/client_serialize.nit --release
-
android/res/: art/icon.svg
../inkscape_tools/bin/svg_to_icons art/icon.svg --android --out android/res/
# Clients and server
-Tinks! has two distinct clients and one dedicated server.
+Tinks! has a client and a dedicated server.
The whole project is modular, these software share mostly the same code.
-* The original 2D client at `bin/tinks` uses mnit and OpenGL ES 1.0 to display the world from above.
-
- The Android variant at `bin/tinks.apk` uses the same view from above, with added on-screen controls.
-
- ![Screenshot of the 2D client](doc/tinks.png)
-
-* The 3D client at `bin/tinks3d` uses gamnit and OpenGL ES 2.0 for an immersive world.
+* The 3D client at `bin/tinks3d` uses `gamnit` and OpenGL ES 2.0 for an immersive world.
Despite the different graphics, both client are fully compatible for multiplayer.
+++ /dev/null
-# This file is part of NIT ( http://www.nitlanguage.org ).
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Android client with a joystick
-module android_client is
- app_name "Tinks!"
- app_namespace "net.xymus.tinks"
- android_manifest """<uses-permission android:name="android.permission.INTERNET" />"""
- android_api_target 10
-end
-
-import mnit::android
-import android::audio
-import android::vibration
-import android::landscape
-
-intrude import client
-import controls
-
-redef class App
-
- # Tank direction control
- var joystick = new Joystick is lazy
-
- redef var controls = new Array[Control].with_items(joystick) is lazy
-
- redef fun input(event)
- do
- var local_player = context.local_player
- var local_tank = local_tank
- if local_player != null and local_tank != null and event isa ControlEvent then
- local_player.orders.add new TankDirectionOrder(local_tank, joystick.value_x, joystick.value_y)
- return true
- end
-
- if event isa AndroidKeyEvent then
- if event.is_back_key then
- quit = true
- native_activity.finish
- return false
- end
- end
-
- return super
- end
-end
-
-redef class ExplosionEvent
- redef fun client_react(display, turn)
- do
- super
-
- var local_tank = app.local_tank
- var d = 20
- if local_tank != null then
- d = local_tank.pos.dist(pos).to_i
- d = 100 - d*5
- d = d.max(10)
- end
-
- app.vibrator.vibrate d
- end
-end
-
-# On-demand joystick that popups up when tapping the left border of the screen
-class Joystick
- super Control
-
- # Current position of the joystick from its center on the X axis, in `[-1.0..1.0]`
- var value_x = 0.0
-
- # Current position of the joystick from its center on the Y axis, in `[-1.0..1.0]`
- var value_y = 0.0
-
- # Deadzone at the center of the joystick where the values are set at 0.0
- var deadzone = 0.3
-
- # Position of the center of the joystick, set at where the screen is first tapped
- var center: nullable ScreenPos
-
- # Position of the top of the joystick, the one that follows the finger
- var handle: nullable ScreenPos
-
- # Id of the pointer/finger that triggered the joystick
- var captured_pointer: Int = -1
-
- # Image of the left border
- var img_zone: Image = app.assets.drawing.joystick_zone
-
- # Image of the joystick base
- var img_back: Image = app.assets.drawing.joystick_back
-
- # Image of the top of the joystick
- var img_handle: Image = app.assets.drawing.joystick_handle
-
- # Radius where the top of the joystick can move around the center
- var radius: Float = img_back.width.to_f / 2.0 - 4.0
-
- # Width of the left border used to trigger the joystick
- var capture_width = 400.0
-
- redef fun draw(display)
- do
- display.blit_stretched(img_zone,
- 0, -128,
- 0, display.height+128,
- capture_width, display.height+128,
- capture_width, -128)
-
- var center = center
- var handle = handle
- if center != null and handle != null then
- img_back.scale = 1.0
- img_handle.scale = 1.0
- display.blit_centered(img_back, center.x, center.y)
- display.blit_centered(img_handle, handle.x, handle.y)
- end
- end
-
- redef fun input(event)
- do
- if event isa AndroidPointerEvent then
- var center = center
- if center == null then
- # New joystick?
- print "New joystick? {event.just_went_down} {event.x} < {capture_width}"
- if event.just_went_down and
- event.x < capture_width then
- # Capture it!
- self.center = new ScreenPos(event.x, event.y)
- self.captured_pointer = event.pointer_id
- self.handle = center
- return true
- end
- else
- # Already down
-
- # Is it the finger already on the joystick?
- if captured_pointer != event.pointer_id then return false
-
- if event.depressed then
- self.center = null
- self.handle = null
- self.value_x = 0.0
- self.value_y = 0.0
- else
- # Update values
- var dx = center.x - event.x
- var dy = center.y - event.y # This is inverted, as is the input
- var d = dx.hypot_with(dy)
- if d < deadzone then
- self.value_x = 0.0
- self.value_y = 0.0
- self.handle = center
- else
- var a = atan2(dx, dy)+pi/2.0
- self.value_x = a.cos
- self.value_y = a.sin
-
- if d > radius then d = radius
- self.handle = new ScreenPos(center.x+a.cos*d, center.y-a.sin*d)
- end
- end
-
- app.input new ControlEvent(self)
- return true
- end
- end
-
- return false
- end
-end
+++ /dev/null
-# This file is part of NIT ( http://www.nitlanguage.org ).
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Client `Assets` and client-only services on game elements
-module assets is no_warning("attr-in-refinement")
-
-import app::audio
-
-import game
-
-import drawing
-
-redef class App
- redef fun load_image(path)
- do
- var img = super
- img.scale = 0.5
- return img
- end
-end
-
-redef class FeatureRule
- # Images of different alternatives
- var images: Array[Image]
-end
-
-redef class TankRule
- # Image of the base tank structure, at different health level
- var base_images: Array[Image]
-
- # Image of the turret
- var turret_image: Image
-end
-
-redef class Feature
- # Rotation angle of this feature
- var angle: Float = pi.rand
-
- # Index within `rule.images` of the image of this instance
- var image_index: Int = rule.images.length.rand is lazy
-end
-
-# Collection of assets
-class Assets
-
- # Images from the `art/drawing.svg` file
- var drawing = new DrawingImages
- init do drawing.load_all(app)
-
- # Firing sound
- var turret_fire = new Sound("sounds/turret_fire.wav")
-
- # Turret is ready to fire sound
- var turret_ready = new Sound("sounds/turret_ready.mp3")
-
- # Associate images to the `story` rules
- fun assign_images_to_story(story: Story)
- do
- story.tree.images = drawing.trees
- story.rock.images = drawing.rock
- story.debris.images = drawing.debris
-
- for tank in story.tanks do
- tank.base_images = drawing.tank_hit
- tank.turret_image = drawing.turret
- end
-
- story.health.images = [drawing.health]
- end
-end
+++ /dev/null
-# This file is part of NIT ( http://www.nitlanguage.org ).
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Portable client
-module client
-
-import mnit
-import mnit::opengles1
-import performance_analysis
-
-import game
-import common
-
-import assets
-import base
-
-# A position within the screen
-class ScreenPos
- super Point[Float]
-
- # Convert to a game logic `Pos` by applying camera transformation
- fun to_logic(camera: Camera): Pos do
- return new Pos(x/camera.basic_zoom + camera.dx, y/camera.basic_zoom + camera.dy)
- end
-end
-
-redef class Pos
- # Convert to a `ScreenPos` by applying camera transformation
- fun to_screen(camera: Camera): ScreenPos do
- return new ScreenPos((x - camera.dx) * camera.basic_zoom, (y - camera.dy) * camera.basic_zoom)
- end
-end
-
-# Camera managing the screen view on the world
-class Camera
- # Offset of the top left corner of the screen, X part
- var dx = 0.0
-
- # Offset of the top left corner of the screen, Y part
- var dy = 0.0
-
- # Basic zoom, the distance between 2 features
- #
- # In the world logic, the distance is of 1.
- # This value depends on the size of the graphical assets.
- #
- # TODO make it a full zoom by scaling images too, if needed.
- var basic_zoom = 32.0
-
- # Center of the `display` as world `Pos`
- fun center(display: Display): Pos
- do
- return (new ScreenPos(display.width.to_f * 0.5, display.height.to_f * 0.5)).to_logic(self)
- end
-
- # Center the `display` on the world `pos`
- fun center_on(display: Display, pos: Pos)
- do
- self.dx = pos.x - display.width.to_f * 0.5 / basic_zoom
- self.dy = pos.y - display.height.to_f * 0.5 / basic_zoom
- end
-end
-
-redef class App
-
- # Collection of assets
- var assets = new Assets is lazy
-
- redef fun on_create
- do
- super
- maximum_fps = 60.0
- assets.assign_images_to_story context.game.story
- end
-
- # Camera managing transformation between world and screen positions
- var camera = new Camera
-
- # Square of the minimum distance from the tank for an object to be "far"
- #
- # This value influences which sounds are heard,
- # the strength of vibrations and
- # whether an arrow points to a far unit
- private var far_dist2 = 2000.0
-
- redef fun context
- do
- var s = super
- if s isa RemoteGameContext then maximum_fps = 0.0
- return s
- end
-
- # Tank tracks tracks on the ground
- #
- # TODO use particles or at least optimize drawing
- var tracks = new List[Couple[Pos, Float]]
-
- redef fun frame_core(display)
- do
- var clock = new Clock
-
- var turn = context.do_turn
- sys.perfs["do_turn"].add clock.lapse
-
- # Draw
-
- # Update camera
- if down_keys.has("left") then camera.dx -= 1.0
- if down_keys.has("right") then camera.dx += 1.0
- if down_keys.has("up") then camera.dy -= 1.0
- if down_keys.has("down") then camera.dy += 1.0
-
- var local_tank = local_tank
- if local_tank != null then
- var tank_speed = local_tank.direction_forwards*local_tank.rule.max_speed
- tank_speed = tank_speed.clamp(-0.5, 0.5)
-
- var prop_pos = local_tank.pos + local_tank.heading.to_vector(tank_speed * 16.0)
- var old_pos = camera.center(display)
- var half = old_pos.lerp(prop_pos, 0.02)
-
- camera.center_on(display, new Pos(half.x, half.y))
- end
-
- # Grass
- display.clear(0.0, 0.45, 0.0)
-
- # Past tank tracks
- for track in tracks do
- var pos = track.first.to_screen(camera)
- display.blit_rotated(assets.drawing.track, pos.x, pos.y, track.second)
- end
-
- # Past blast sites
- for blast in context.game.world.blast_sites do
- var pos = blast.to_screen(camera)
- display.blit_centered(assets.drawing.blast, pos.x, pos.y)
- end
-
- # Terrain features
- var tl = (new ScreenPos(0.0, 0.0)).to_logic(camera)
- var br = (new ScreenPos(display.width.to_f, display.height.to_f)).to_logic(camera)
- for x in [tl.x.floor.to_i .. br.x.ceil.to_i] do
- for y in [tl.y.floor.to_i .. br.y.ceil.to_i] do
- var feature = context.game.world[x, y]
- if feature != null then
- var pos = feature.pos.to_screen(camera)
- var image = feature.rule.images[feature.image_index]
- display.blit_rotated(image, pos.x, pos.y, feature.angle)
- end
- end
- end
-
- # Tanks
- for tank in context.game.tanks do
- # Add random tracks
- if (tank.direction_heading != 0.0 and 40.rand == 0) or
- (tank.direction_forwards != 0.0 and 100.rand == 0) then
-
- tracks.add new Couple[Pos, Float](tank.pos, tank.heading)
- if tracks.length > 1000 then tracks.shift
- end
-
- # Get the player stencil
- var player = tank.player
- var stencil = null
- if player != null then stencil = assets.drawing.stencils[player.stencil_index]
-
- if camera.center(display).dist2(tank.pos) > far_dist2 then
- var hw = (display.width/2).to_f
- var hh = (display.height/2).to_f
-
- var angle = camera.center(display).atan2(tank.pos)
- var x = hw + angle.cos * (hw-128.0)
- var y = hh + angle.sin * (hh-128.0)
-
- var screen_pos = new ScreenPos(x, y)
- display.blit_rotated(assets.drawing.arrow, screen_pos.x, screen_pos.y, angle)
- if stencil != null then display.blit_rotated(stencil, screen_pos.x, screen_pos.y, angle)
- continue
- end
-
- var screen_pos = tank.pos.to_screen(camera)
-
- var damage = tank.rule.max_health - tank.health
- damage = damage.clamp(0, tank.rule.base_images.length)
-
- var base_image = tank.rule.base_images[damage]
- display.blit_rotated(base_image, screen_pos.x, screen_pos.y, tank.heading)
- if stencil != null then display.blit_rotated(stencil, screen_pos.x, screen_pos.y, tank.heading)
- display.blit_rotated(tank.rule.turret_image, screen_pos.x, screen_pos.y, tank.turret.heading)
-
- if debug then
- var corners = tank.corners_at(new Couple[Pos, Float](tank.pos, tank.heading))
- for c in corners do
- var p = c.to_screen(camera)
- display.blit_centered(assets.drawing.red_dot, p.x, p.y)
- end
- end
- end
-
- # Events
- for event in turn.events do event.client_react(display, turn)
-
- # Gather and show some performance stats!
- sys.perfs["draw"].add clock.lapse
- if context.game.tick % 300 == 5 then print sys.perfs
- end
-
- # Keys currently down
- #
- # TODO find a nice API and move up to mnit/gamnit
- var down_keys = new HashSet[String]
-
- redef fun input(ie)
- do
- var local_tank = local_tank
- var local_player = context.local_player
-
- # Quit?
- if ie isa QuitEvent or
- (ie isa KeyEvent and ie.name == "escape") then
-
- quit = true
- return true
- end
-
- # Spawn a new tank?
- if local_tank == null and local_player != null then
- if (ie isa KeyEvent and ie.name == "space") or
- (ie isa PointerEvent and ie.depressed) then
-
- local_player.orders.add new SpawnTankOrder(local_player)
- return true
- end
- end
-
- if ie isa KeyEvent then
-
- # Update `down_keys`
- var name = ie.name
- if ie.is_down then
- down_keys.add name
- else if down_keys.has(name) then
- down_keys.remove name
- end
-
- # wasd to move tank
- var direction_change = ["w", "a", "s", "d"].has(ie.name)
- if direction_change and local_tank != null and local_player != null then
- var forward = down_keys.has("w")
- var backward = down_keys.has("s")
- var left = down_keys.has("a")
- var right = down_keys.has("d")
-
- # Cancel contradictory commands
- if forward and backward then
- forward = false
- backward = false
- end
-
- if left and right then
- left = false
- right = false
- end
-
- # Set movement and direction
- var move = 0.0
- if forward then
- move = 0.5
- else if backward then move = -0.5
-
- var ori = 0.0
- if left then
- ori = -local_tank.rule.max_direction/2.0
- else if right then ori = local_tank.rule.max_direction/2.0
-
- # Activate to invert the orientation on reverse, (for at @R4p4Ss)
- #if backward then ori = -ori
-
- # Bonus when only moving or only turning
- if not forward and not backward then ori *= 2.0
- if not left and not right then move *= 2.0
-
- # Give order
- local_player.orders.add new TankDirectionOrder(local_tank, ori, move)
- return true
- end
- end
-
- # On click (or tap), aim and fire
- if ie isa PointerEvent then
-
- if ie.pressed and local_tank != null and local_player != null then
- var target = (new ScreenPos(ie.x, ie.y)).to_logic(camera)
- local_player.orders.add new AimAndFireOrder(local_tank, target)
- return true
- end
- end
-
- return false
- end
-end
-
-redef class TEvent
- fun client_react(display: Display, turn: TTurn) do end
-end
-
-redef class ExplosionEvent
- redef fun client_react(display, turn)
- do
- var pos = pos.to_screen(app.camera)
- display.blit_centered(app.assets.drawing.explosion, pos.x, pos.y)
- end
-end
-
-redef class OpenFireEvent
- redef fun client_react(display, turn)
- do
- var screen_pos = tank.pos.to_screen(app.camera)
- display.blit_rotated(app.assets.drawing.turret_firing, screen_pos.x, screen_pos.y, tank.turret.heading)
-
- if tank.pos.dist2(app.camera.center(display)) < app.far_dist2 then
- # Within earshot
- app.assets.turret_fire.play
- end
- end
-end
-
-redef class TurretReadyEvent
- redef fun client_react(display, turn)
- do
- if tank.pos.dist2(app.camera.center(display)) < app.far_dist2 then
- # Within earshot
- app.assets.turret_ready.play
- end
- end
-end
+++ /dev/null
-# This file is part of NIT ( http://www.nitlanguage.org ).
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# On-screen alternative controls
-module controls
-
-import client
-
-redef class App
-
- # All active UI controls
- fun controls: Array[Control] is abstract
-
- redef fun frame_core(display)
- do
- super
-
- for control in controls do control.draw(display)
- end
-
- redef fun input(event)
- do
- for control in controls do
- var hit = control.input(event)
- if hit then return true
- end
-
- return super
- end
-end
-
-# An event raised by a `Control`
-class ControlEvent
- super InputEvent
-
- # Sender control
- var sender: Control
-end
-
-# UI control
-abstract class Control
-
- # Draw `self` to `display`
- fun draw(display: Display) do end
-
- # Intercept and act upon events concerving `self`
- fun input(event: InputEvent): Bool do return false
-end
+++ /dev/null
-# This file is part of NIT ( http://www.nitlanguage.org ).
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# GNU/Linux client with config saved to `config.json`
-module linux_client
-
-import mnit::linux
-import linux::audio
-import json
-
-import client
-
-# Configuration of the client
-class ClientConfig
- serialize
-
- # Resolution width
- var res_x = 1920 is lazy
-
- # Resolution height
- var res_y = 1080 is lazy
-
- # Should the client play sounds?
- var play_sounds = true is lazy
-end
-
-redef class App
- private var config_path: String = sys.program_name.dirname / "../config.json"
-
- private var config: ClientConfig do
- if config_path.file_exists then
- var content = config_path.to_path.read_all
- var deser = new JsonDeserializer(content)
- var cc = deser.deserialize
-
- if cc == null then
- print_error "Client Error: Deserializing config file failed with {deser.errors.join(", ")}"
- else if not cc isa ClientConfig then
- print_error "Client Error: Deserializing config file failed, got '{cc}'"
- # TODO simplify the previous lines with ? or similar
- else return cc
- end
-
- # Save the default config to pretty Json
- var cc = new ClientConfig
- var json = cc.serialize_to_json(plain=true, pretty=true)
- json.write_to_file config_path
-
- return cc
- end
-end
-
-redef class Display
- redef fun wanted_width do return app.config.res_x
- redef fun wanted_height do return app.config.res_y
-end
-
-redef class Sound
- redef fun play do if app.config.play_sounds then super
-end
-
-redef class JsonDeserializer
- # The only class we deserialize from pretty Json is ClientConfig
- redef fun class_name_heuristic(object) do return "ClientConfig"
-end