# 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 join_state_updated(state: JoinState) signal text_message(message: TextMessage) signal update_tutorial_running(running: bool) class TextMessage: var username: String var color: Color var text: String var timeout_initial: float var timeout_remaining enum SpectatingMode { CENTER, FREE, } enum JoinState { SPECTATING, WAITING, JOINED, } var player_id: float = -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 hand_count = 0 var text_message_history: Array[TextMessage] = [] var join_state: JoinState = JoinState.SPECTATING var in_lobby := false var is_replay := false var tutorial_running := false var tutorial_queue := [] 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 pinned_items: PinnedItemMessages = $"../PinnedItemMessages" @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") ) mp.connect_to_url(menu.data) 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"] hand_count = p["data"]["hand_count"] 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 Global.last_map_name = Global.current_map_name Global.current_map_name = p["data"]["current_map"] data_updated.emit() "add_player": var player_instance: Player if p.id == player_id: player_instance = ControllablePlayer.new(p.id, p.name, p.position, p.character, p.class, 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 set_join_state(JoinState.JOINED) else: player_instance = Player.new(p.id, p.name, p.position, p.character, p.class, self) players[p.id] = player_instance add_child(player_instance) update_players.emit(players) "remove_player": var player: Player = players.get(p.id) if player == null: return if player.is_customer and player.current_item_message != null: tutorial_queue.erase(player.current_item_message) pinned_items.clear_item(p.id) if p.id == player_id: set_join_state(JoinState.SPECTATING) camera.target = $Center for h in player.hand: if h != null: h.queue_free() players.erase(p.id) player.queue_free() update_players.emit(players) "movement": var player_instance: Player = players[p.player] player_instance.update_position(p.pos, p.rot, p.boost) if p.player == player_id: last_position = p.pos "movement_sync": if not players.has(player_id): return var player_instance: ControllablePlayer = players[player_id] player_instance.position_ = last_position "move_item": if "player" in p.from and "player" in p.to: players[p.from.player[0]].pass_to(players[p.to.player[0]], int(p.from.player[1]), int(p.to.player[1])) elif "tile" in p.from and "player" in p.to: var t: Tile = map.get_tile_instance(p.from.tile) players[p.to.player[0]].take_item(t, int(p.to.player[1])) elif "player" in p.from and "tile" in p.to: var t: Tile = map.get_tile_instance(p.to.tile) players[p.from.player[0]].put_item(t, int(p.from.player[1])) elif "tile" in p.from and "tile" in p.to: var from_tile2: Tile = map.get_tile_instance(p.from.tile) var to_tile2: Tile = map.get_tile_instance(p.to.tile) from_tile2.pass_to(to_tile2) "set_progress": if "tile" in p.item: var t: Tile = map.get_tile_instance(p.item.tile) t.progress(p.position, p.speed, p.warn, players.get(p.player)) else: players[p.item.player[0]].progress(p.position, p.speed, p.warn, int(p.item.player[1])) "clear_progress": if "tile" in p.item: var t: Tile = map.get_tile_instance(p.item.tile) t.finish() else: players[p.item.player[0]].finish(int(p.item.player[1])) "set_item": var location: Dictionary = p["location"] if p.item != null: if "tile" in p.location: var t: Tile = map.get_tile_instance(p.location.tile) var i = ItemFactory.produce(item_names[p.item], t.item_base) i.position = t.item_base.global_position add_child(i) i.name = item_names[p.item] t.set_item(i) else: var pl: Player = players[p.location.player[0]] var h = p.location.player[1] var i = ItemFactory.produce(item_names[p.item], pl.hand_base[h]) i.position = pl.hand_base[h].global_position add_child(i) i.name = item_names[p.item] pl.set_item(i, h) else: if "tile" in p.location: var t: Tile = map.get_tile_instance(p.location.tile) t.finish() t.set_item(null) else: var pl: Player = players[p.location.player[0]] var h = p.location.player[1] pl.finish(h) pl.set_item(null, h) "update_map": var neighbors: Array = p["neighbors"] if p.kind != null: if neighbors != null: neighbors = neighbors.map(func(x): return tile_names[x] if x != null else null) map.set_tile(p.tile, tile_names[p.kind], neighbors) else: map.clear_tile(p.tile) "flush_map": map.flush() "communicate": 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. var pinned: bool = p.timeout.pinned if p.timeout != null and "pinned" in p.timeout else false if p.message != null: if "item" in p.message: var item_name: String = item_names[p.message.item] var parsed_item := Global.ParsedItem.new(item_name) var ingredients := [parsed_item.name] ingredients.append_array(parsed_item.contents) if pinned: pinned_items.pin_item(item_name, timeout_initial, timeout_remaining, p.player) var player: Player = players[p.player] player.item_message(item_name, timeout_initial, timeout_remaining) # Maybe start tutorial # after joining, the last item message that popped up is ignored. the next one will # be used for the tutorial if (player.is_customer and not Global.get_setting("gameplay.tutorial_disabled") and join_state == JoinState.JOINED): var completed_ingredients: Array = Global.get_profile("tutorial_ingredients_played") var completed := Global.array_has_all(completed_ingredients, ingredients) if not completed: if tutorial_running: tutorial_queue.push_back(item_name) else: tutorial_running = true update_tutorial_running.emit(tutorial_running) mp.send_chat(player_id, "/start-tutorial %s" % item_name) elif "text" in p.message or "translation" in p.message: var data = TextMessage.new() data.timeout_initial = timeout_initial data.timeout_remaining = timeout_remaining if pinned: push_error("Pinned text messages are currently not supported") var player: Player = players[p.player] data.color = Character.COLORS[player.character.ParsedStyle.new(player.character_idx).color] data.username = players[p.player].username data.text = p.message.text if "text" in p.message else get_message_str(p.message) player.text_message(data) text_message.emit(data) text_message_history.append(data) else: push_error("neither text, item nor effect provided") else: var player: Player = players[p.player] if player.is_customer and player.current_item_message != null: tutorial_queue.erase(player.current_item_message) player.clear_message() pinned_items.clear_item(p.player) "effect": players[p.player].effect_message(p.name) "set_ingame": in_lobby = p.lobby overlay.set_ingame(p.state, p.lobby) follow_camera.set_ingame(p.state, p.lobby) if p.state: map.gi_bake() await get_parent()._menu_open() map.autobake = true in_lobby_updated.emit(in_lobby) else: map.autobake = false await get_parent()._menu_exit() lobby.visible = in_lobby if in_lobby: lobby.select_map(0) if lobby and join_state == JoinState.SPECTATING: if in_lobby: toggle_join() else: menu.submenu("res://menu/ingame.tscn") "score": if p.time_remaining != null: overlay.update(p.demands_failed, p.demands_completed, p.points, p.time_remaining) "tutorial_ended": if p.player != player_id: return tutorial_running = false update_tutorial_running.emit(tutorial_running) if p.success: var completed_item := Global.ParsedItem.new(item_names[p.item]) var played: Array = Global.get_profile("tutorial_ingredients_played") played.append(completed_item.name) played.append_array(completed_item.contents) Global.set_profile("tutorial_ingredients_played", played) Global.save_profile() while item_names[p.item] in tutorial_queue: tutorial_queue.erase(item_names[p.item]) if not tutorial_queue.is_empty() and not Global.get_setting("gameplay.tutorial_disabled"): tutorial_running = true update_tutorial_running.emit(tutorial_running) mp.send_chat(player_id, "/start-tutorial %s" % tutorial_queue.pop_front()) else: tutorial_queue.clear() "menu": match p.menu: "document": menu.submenu("res://menu/document/document.tscn", p["data"]) "score": menu.submenu("res://menu/rating/rating.tscn", [p.data.stars, p.data.points]) "server_message": var mstr := get_message_str(p.message) if p.error: popup_message.display_server_msg(tr("c.error.server").format([mstr])) push_error(tr("c.error.server").format([mstr])) else: popup_message.display_server_msg(mstr) "server_hint": if p.player != player_id: return var message = p.get("message") var position_ = p.get("position") if position_ == null: # Global hint message if message == null: popup_message.clear_server_msg() else: popup_message.display_server_msg(get_message_str(message), false) else: # Positional hint message if message == null: popup_message.clear_server_msg(position_) else: popup_message.display_server_msg_positional(get_message_str(message), position_, false) "environment": $Environment.update(p.effects) "redirect": get_parent().replace_menu("res://menu/game.tscn", p.uri[0]) "replay_start": is_replay = true _: push_error("Unrecognized packet type: %s" % p.type) func set_join_state(state: JoinState): join_state = state join_state_updated.emit(state) func toggle_join(): match join_state: JoinState.SPECTATING: set_join_state(JoinState.WAITING) mp.send_join(Global.get_profile("username"), Global.get_profile("character_style")) JoinState.WAITING: push_error("Join/Leave action already toggled.") JoinState.JOINED: set_join_state(JoinState.WAITING) mp.send_leave(player_id) func _process(delta): update_center() if is_replay and mp != null: mp.send_replay_tick(delta) # TODO: move into PopupMessage and use RichTextLabel for tile/item images func get_message_str(m: Dictionary) -> String: if "translation" in m: return tr(m.translation.id).format(m.translation.params.map(get_message_str)) if "tile" in m.keys(): return tile_names[m.tile] if "item" in m.keys(): return item_names[m.item] return Global.get_message_str(m) 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 join_state != JoinState.SPECTATING: 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 no_chefs = true for v in players.values(): no_chefs = no_chefs or v.is_chef var sum: int = 0 var center: Vector3 = Vector3(0.,0.,0.) for v in players.values(): var p: Player = v sum += 1 center += p.movement_base.position var bmin = Vector2.INF var bmax = -Vector2.INF for p in players.values(): if !p.is_customer or no_chefs: bmin.x = min(bmin.x, p.movement_base.position.x) bmin.y = min(bmin.y, p.movement_base.position.z) bmax.x = max(bmax.x, p.movement_base.position.x) bmax.y = max(bmax.y, p.movement_base.position.z) var extent = max(bmax.x - bmin.x, bmax.y - bmin.y) if sum > 0: $Center.position = center / sum $FollowCamera.camera_distance_target = max(extent * 2, 5) elif sum > 0: $Center.position = center / sum $FollowCamera.camera_distance_target = max(extent * 2, 5) else: var extents = map.extents() var map_center = ((extents[0] + extents[1]) / 2) + Vector2(.5, .5) $Center.position = Vector3(map_center.x, 0.,map_center.y) $FollowCamera.camera_distance_target = (extents[1] - extents[0]).length() / 2 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)