# Hurry Curry! - a game about cooking # Copyright 2024 nokoe # Copyright 2024 metamuffin # Copyright 2024 tpart # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, version 3 of the License only. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # class_name Game extends Node3D signal update_players(players: Dictionary) signal data_updated() signal in_lobby_updated(in_lobby: bool) signal text_message(player: int, text: String, timeout_initial: float, timeout_remaining: float) signal joined() signal left() enum SpectatingMode { CENTER, FREE, } var player_id: int = -1 var item_names: Array = [] var item_index_by_name: Dictionary = {} var tile_names: Array = [] var tile_index_by_name: Dictionary = {} var tile_collide: Array = [] var tile_interact: Array = [] var maps: Array = [] var bot_algos: Array var text_message_history: Array[Array] = [] var in_lobby := false var is_replay := false var is_joined := false var join_sent := false var last_position := Vector2(0, 0) var players := {} var spectating_mode: SpectatingMode = SpectatingMode.CENTER @onready var camera: FollowCamera = $FollowCamera @onready var mp: Multiplayer = $Multiplayer @onready var map: Map = $Map @onready var lobby: Lobby = $"../Lobby" @onready var overlay: Overlay = $"../Overlay" @onready var popup_message: PopupMessage = $"../PopupMessage" @onready var menu: GameMenu = $".." @onready var follow_camera: FollowCamera = $FollowCamera func _ready(): mp.packet.connect(handle_packet) mp.connection_closed.connect(func(reason: String): Global.error_message = reason; get_parent().replace_menu("res://menu/error.tscn") ) func handle_packet(p): match p.type: "joined": player_id = p["id"] "data": item_names = p["data"]["item_names"] tile_names = p["data"]["tile_names"] tile_collide = p["data"]["tile_collide"] tile_interact = p["data"]["tile_interact"] maps = p["data"]["maps"] bot_algos = p["data"]["bot_algos"] tile_index_by_name.clear() for id in tile_names.size(): tile_index_by_name[tile_names[id]] = id item_index_by_name.clear() for i in range(item_names.size()): item_index_by_name[item_names[i]] = i data_updated.emit() "add_player": var id = p["id"] var player_name = p["name"] var pos = pos_to_vec2(p["position"]) var character = p["character"] var player_instance: Player if id == player_id: player_instance = ControllablePlayer.new(id, player_name, pos, character, self) in_lobby_updated.connect(player_instance.onscreen_controls.in_lobby_updated) player_instance.onscreen_controls.in_lobby_updated(in_lobby) camera.target = player_instance.movement_base is_joined = true join_sent = true joined.emit() else: player_instance = Player.new(id, player_name, pos, character, self) players[id] = player_instance add_child(player_instance) update_players.emit(players) "remove_player": var player: Player = players.get(p.id) if p.id == player_id: is_joined = false join_sent = false left.emit() camera.target = $Center if player != null: if player.hand != null: player.hand.queue_free() players.erase(p.id) player.queue_free() update_players.emit(players) "movement": var player = p["player"] var pos = pos_to_vec2(p["pos"]) var rot = p["rot"] var boost = p["boost"] var dir = pos_to_vec2(p["dir"]) var player_instance: Player = players[player] player_instance.update_position(pos, rot, boost) if player == player_id: last_position = pos "movement_sync": if not players.has(player_id): return var player_instance: ControllablePlayer = players[player_id] player_instance.position_ = last_position "move_item": var from: Dictionary = p["from"] var to: Dictionary = p["to"] var from_player = from.get("player") var from_tile = from.get("tile") var to_player = to.get("player") var to_tile = to.get("tile") if from_player != null and to_player != null: players[from_player].pass_to(players[to_player]) elif from_tile != null and to_player != null: var t: Tile = map.get_tile_instance(pos_to_vec2i(from_tile)) players[to_player].take_item(t) elif from_player != null and to_tile != null: var t: Tile = map.get_tile_instance(pos_to_vec2i(to_tile)) players[from_player].put_item(t) elif from_tile != null and to_tile != null: var from_tile2: Tile = map.get_tile_instance(pos_to_vec2i(from_tile)) var to_tile2: Tile = map.get_tile_instance(pos_to_vec2i(to_tile)) from_tile2.pass_to(to_tile2) "set_progress": var warn: bool = p["warn"] var position: float = p["position"] var speed: float = p["speed"] var item: Dictionary = p["item"] var tile = item.get("tile") var player = item.get("player") var acting_player = p.get("player") if tile != null: @warning_ignore("incompatible_ternary") var t: Tile = map.get_tile_instance(pos_to_vec2i(tile)) t.progress(position, speed, warn, players.get(int(acting_player) if acting_player != null else null)) else: players[player].progress(position, speed, warn) "clear_progress": var item: Dictionary = p["item"] var tile = item.get("tile") var player = item.get("player") if tile != null: var t: Tile = map.get_tile_instance(pos_to_vec2i(tile)) t.finish() else: players[player].finish() "set_item": var location: Dictionary = p["location"] var tile = location.get("tile") var player = location.get("player") var item = p.get("item") if item != null: if tile != null: var t: Tile = map.get_tile_instance(pos_to_vec2i(tile)) var i = ItemFactory.produce(item_names[item], t.item_base) i.position = t.item_base.global_position add_child(i) i.name = item_names[item] t.set_item(i) else: var pl: Player = players[player] var i = ItemFactory.produce(item_names[item], pl.hand_base) add_child(i) i.name = item_names[item] pl.set_item(i) else: if tile != null: var t: Tile = map.get_tile_instance(pos_to_vec2i(tile)) t.set_item(null) else: players[player].set_item(null) "update_map": var tile: Vector2i = pos_to_vec2i(p["tile"]) var kind = p.get("kind") var neighbors: Array = p["neighbors"] if kind != null: if neighbors != null: neighbors = neighbors.map(func(x): return tile_names[x] if x != null else null) map.set_tile(tile, tile_names[kind], neighbors) else: map.clear_tile(tile) "communicate": var player: int = p["player"] var message = p.get("message") var timeout_initial: float = p["timeout"]["initial"] if p["timeout"] != null else 5. var timeout_remaining: float = p["timeout"]["remaining"] if p["timeout"] != null else 5. if message != null: var item = message.get("item") var text = message.get("text") var effect = message.get("effect") if item != null: players[player].item_message(item_names[item], timeout_initial, timeout_remaining) elif text != null: players[player].text_message(text, timeout_initial, timeout_remaining) var username: String = players[player].username text_message.emit(username, text, timeout_initial, timeout_remaining) text_message_history.append([username, text]) else: push_error("neither text, item nor effect provided") else: players[player].clear_message() "effect": var player: int = p["player"] var name: int = p["name"] players[player].effect_message(name) "set_ingame": var state = p["state"] var lob = p["lobby"] in_lobby = lob in_lobby_updated.emit(in_lobby) if state: map.gi_bake() await get_parent()._menu_open() map.autobake = true if lobby: popup_message.lobby() else: popup_message.ingame() else: map.autobake = false await get_parent()._menu_exit() lobby.visible = in_lobby if lobby and not join_sent: join() overlay.set_ingame(state, lob) follow_camera.set_ingame(state, lob) "error": var message = p["message"] push_warning("server error: %s" % message) "score": var demands_failed: int = p["demands_failed"] var demands_completed: int = p["demands_completed"] var points: int = p["points"] var time_remaining = p.get("time_remaining") if time_remaining != null: overlay.update(demands_failed, demands_completed, points, time_remaining) "menu": var menu_type: String = p["menu"] match menu_type: "book": menu.submenu("res://menu/book/book.tscn") "score": menu.submenu("res://menu/rating/rating.tscn", [p.data.stars, p.data.score]) "server_message": var message = p["message"] var message_str := get_message_str(message) if error: # TODO: Custom popup message style for errors popup_message.display_server_msg(message_str) push_error("Server error: %s" % message_str) else: popup_message.display_server_msg(message_str) "environment": $Environment.update(p["effects"]) "replay_start": is_replay = true _: push_error("Unrecognized packet type: %s" % p.type) func join(): join_sent = true mp.send_join(Global.get_profile("username"), Global.get_profile("character")) func leave(): join_sent = false mp.send_leave(player_id) func _process(delta): update_center() if is_replay and mp != null: mp.send_replay_tick(delta) func get_message_str(m: Dictionary) -> String: if "text" in m: return m.text if "translation" in m: return tr(m.translation.id) % m.translation.params.map(get_message_str) if "tile" in m: return tile_names[m.tile] if "item" in m: return item_names[m.item] return "[unknown message type]" func get_tile_collision(pos: Vector2i) -> bool: var t = map.get_tile_name(pos) if t == null: return true else: return tile_collide[tile_index_by_name[t]] func get_tile_interactive(pos: Vector2i) -> bool: var t = map.get_tile_name(pos) if t == null: return false else: return tile_interact[tile_index_by_name[t]] func update_center(): if is_joined: return if Input.get_vector("left", "right", "forwards", "backwards").normalized().length() > .1: spectating_mode = SpectatingMode.FREE elif spectating_mode == SpectatingMode.FREE and Input.is_action_just_pressed("reset"): spectating_mode = SpectatingMode.CENTER match spectating_mode: SpectatingMode.CENTER: spectate_center() SpectatingMode.FREE: spectate_free() func spectate_center(): var sum: int = 0 var player_sum: int = 0 var center: Vector3 = Vector3(0.,0.,0.) var player_center: Vector3 = Vector3(0.,0.,0.) for v in players.values(): var p: Player = v if p.character_idx >= 0: player_sum += 1 player_center += p.movement_base.position sum += 1 center += p.movement_base.position var new_center: Vector3 = Vector3(0.,0.,0.) if player_sum > 0: new_center = player_center / player_sum elif sum > 0: new_center = center / sum else: var extents = map.extents() var map_center = ((extents[0] + extents[1]) / 2) + Vector2(.5, .5) new_center = Vector3(map_center.x, 0.,map_center.y) $Center.position = new_center func spectate_free(): var direction := Input.get_vector("left", "right", "forwards", "backwards") direction = direction.rotated(-camera.angle_target) $Center.position += Vector3( direction.x, $Center.position.y, direction.y ) * get_process_delta_time() * 10. var extents = map.extents() $Center.position.x = clamp($Center.position.x, extents[0].x, extents[1].x) $Center.position.z = clamp($Center.position.z, extents[0].y, extents[1].y) func pos_to_vec2(pos: Array) -> Vector2: return Vector2(pos[0], pos[1]) func pos_to_vec2i(pos: Array) -> Vector2i: return Vector2i(pos[0], pos[1])