extends Node class_name Tasks ## The job. Deliberately, precisely annoying. ## ## Stations sit in the world (a desk, the photocopier). You walk up, press E, and get ## locked in place doing something mundane — because you ARE at your desk, and being ## unable to walk away is part of it. Every station has a designed way of wasting your ## time, and every wasted second is fuel for Rage. ## ## SPREADSHEET — enter a figure in a named cell. The selection silently jumps to a ## neighbouring cell partway through. Commit without noticing and the ## figure lands in the wrong cell and you start again. This one is the ## whole thesis of the game in about forty lines. ## COPIER ...... run a copy job. It jams. Clearing it needs the RIGHT tray, named in ## tiny text, and the trays are not numbered in a sensible order. ## ## Completing work pays WAGES and soothes the meter a little — never quite enough. ## ## Godot 4.7 GDScript 2.0. enum Kind { SPREADSHEET, COPIER } enum Phase { IDLE, SPREADSHEET, COPIER_RUN, COPIER_JAM, DONE } const REACH := 2.1 ## metres you must be within to work at a station const COLS := ["A", "B", "C", "D", "E", "F"] const ROWS := 5 ## Trays are labelled in the order someone at head office thought was obvious. const TRAYS := ["1", "2", "2A", "3", "3B"] @export var wage_per_task: int = 120 @export var soothe_per_task: float = 0.12 var rage: Rage var _player: Node3D var _stations: Array = [] # {kind, pos, label} var _near: Dictionary = {} # the station currently in reach, or empty var phase: int = Phase.IDLE var wages: int = 0 var tasks_done: int = 0 var tasks_fumbled: int = 0 # --- spreadsheet state var _target_cell := Vector2i.ZERO # where the figure is SUPPOSED to go var _sel := Vector2i.ZERO # where the cursor actually is var _want_value := "" var _typed := "" var _jump_at := 0.0 # seconds into the task the selection will drift var _jumped := false var _t := 0.0 # --- copier state var _pages_left := 0 var _job_pages := 0 var _copy_t := 0.0 var _jam_tray := "" var _jam_in := 0.0 signal task_completed() signal task_fumbled(reason: String) func setup(player: Node3D, r: Rage) -> void: _player = player rage = r func register(kind: int, pos: Vector3, label: String) -> void: _stations.append({"kind": kind, "pos": pos, "label": label}) ## Swapping sites invalidates every station position. func clear_stations() -> void: _stations.clear() _near = {} phase = Phase.IDLE func is_working() -> bool: return phase != Phase.IDLE and phase != Phase.DONE ## What the HUD should show as an interaction prompt, or "". func prompt() -> String: if is_working(): return "" if _near.is_empty(): return "" return "E — %s" % String(_near["label"]) func nearest_label() -> String: return String(_near["label"]) if not _near.is_empty() else "" # ---------------------------------------------------------------- proximity func _process(dt: float) -> void: _t += dt if not is_working(): _update_near() return match phase: Phase.SPREADSHEET: _tick_spreadsheet(dt) Phase.COPIER_RUN: _tick_copier(dt) Phase.COPIER_JAM: pass # waits on input # working cleanly is mildly calming, which is what makes the drift so infuriating if rage != null and phase != Phase.COPIER_JAM: rage.soothe(rage.focus_soothe * dt) func _update_near() -> void: _near = {} if _player == null: return var best := REACH * REACH for s in _stations: var d: float = (_player.global_position - (s["pos"] as Vector3)).length_squared() if d < best: best = d _near = s # ---------------------------------------------------------------- entry / exit ## Player pressed E. Returns true if it started (so the caller can skip other uses of E). func try_start() -> bool: if is_working() or _near.is_empty(): return false match int(_near["kind"]): Kind.SPREADSHEET: _begin_spreadsheet() Kind.COPIER: _begin_copier() return true func abandon() -> void: if not is_working(): return phase = Phase.IDLE tasks_fumbled += 1 task_fumbled.emit("walked away") if rage != null: rage.provoke(0.05, "you'll have to start that again") ## You snapped. You are no longer at your desk — the chair is somewhere behind you. ## Unlike abandon() this costs nothing: you didn't choose to walk away from the job. func eject() -> void: if not is_working(): return phase = Phase.IDLE func _finish() -> void: phase = Phase.IDLE tasks_done += 1 wages += wage_per_task task_completed.emit() if rage != null: rage.soothe(soothe_per_task) func _fumble(reason: String, amount: float) -> void: tasks_fumbled += 1 task_fumbled.emit(reason) if rage != null: rage.provoke(amount, reason) # ---------------------------------------------------------------- spreadsheet func _begin_spreadsheet() -> void: phase = Phase.SPREADSHEET _target_cell = Vector2i(randi() % COLS.size(), randi() % ROWS) _sel = _target_cell _want_value = str(randi_range(1000, 9999)) _typed = "" _jumped = false # it drifts once, early enough that you've started typing and are looking at the keys _jump_at = _t + randf_range(1.6, 3.4) func _tick_spreadsheet(_dt: float) -> void: if _jumped or _t < _jump_at: return _jumped = true # nudge the selection one cell in a random direction. No sound, no animation. var dirs := [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)] var d: Vector2i = dirs[randi() % dirs.size()] var moved := Vector2i(clampi(_sel.x + d.x, 0, COLS.size() - 1), clampi(_sel.y + d.y, 0, ROWS - 1)) if moved == _sel: # clamped against an edge; go the other way moved = Vector2i(clampi(_sel.x - d.x, 0, COLS.size() - 1), clampi(_sel.y - d.y, 0, ROWS - 1)) _sel = moved func _key_spreadsheet(k: int) -> void: if k >= KEY_0 and k <= KEY_9: if _typed.length() < 6: _typed += str(k - KEY_0) return match k: KEY_BACKSPACE: _typed = _typed.substr(0, maxi(_typed.length() - 1, 0)) KEY_LEFT: _sel.x = clampi(_sel.x - 1, 0, COLS.size() - 1) KEY_RIGHT: _sel.x = clampi(_sel.x + 1, 0, COLS.size() - 1) KEY_UP: _sel.y = clampi(_sel.y - 1, 0, ROWS - 1) KEY_DOWN: _sel.y = clampi(_sel.y + 1, 0, ROWS - 1) KEY_ENTER, KEY_KP_ENTER: _commit_cell() func _commit_cell() -> void: if _typed != _want_value: _fumble("#VALUE! — that isn't the figure", 0.09) _typed = "" return if _sel != _target_cell: # the drift got you. The figure is now in the wrong cell and you have to redo it. _fumble("it went in %s%d. it was meant to go in %s%d." % [ COLS[_sel.x], _sel.y + 1, COLS[_target_cell.x], _target_cell.y + 1], 0.17) _typed = "" _sel = _target_cell _jumped = false _jump_at = _t + randf_range(2.0, 4.0) return _finish() # ---------------------------------------------------------------- copier func _begin_copier() -> void: phase = Phase.COPIER_RUN _job_pages = randi_range(8, 18) _pages_left = _job_pages _copy_t = 0.0 _jam_in = randf_range(1.2, 2.8) func _tick_copier(dt: float) -> void: _copy_t += dt if _copy_t >= 0.42: _copy_t = 0.0 _pages_left -= 1 if _pages_left <= 0: _finish() return _jam_in -= dt if _jam_in <= 0.0: phase = Phase.COPIER_JAM _jam_tray = TRAYS[randi() % TRAYS.size()] if rage != null: rage.provoke(0.06, "PAPER JAM") func _key_copier(k: int) -> void: if phase != Phase.COPIER_JAM: return var pressed := "" if k >= KEY_1 and k <= KEY_5: pressed = TRAYS[k - KEY_1] if pressed == "": return if pressed == _jam_tray: phase = Phase.COPIER_RUN _jam_in = randf_range(2.0, 5.5) else: _fumble("that's not tray %s" % _jam_tray, 0.08) # ---------------------------------------------------------------- input ## Player forwards keys here while a task is open. Returns true if consumed. func handle_key(k: int) -> bool: if not is_working(): return false if k == KEY_ESCAPE or k == KEY_E: abandon() return true match phase: Phase.SPREADSHEET: _key_spreadsheet(k) Phase.COPIER_RUN, Phase.COPIER_JAM: _key_copier(k) return true # ---------------------------------------------------------------- HUD feed ## Everything the task panel needs to draw itself, as plain data. func panel_state() -> Dictionary: match phase: Phase.SPREADSHEET: return { "kind": "spreadsheet", "title": "QUARTERLY RETURNS.XLS", "instruction": "Enter %s in cell %s%d" % [ _want_value, COLS[_target_cell.x], _target_cell.y + 1], "cols": COLS, "rows": ROWS, "sel": _sel, "typed": _typed, "footer": "digits · arrows move · ENTER commit · E leave", } Phase.COPIER_RUN: return { "kind": "copier", "title": "MULTIFUNCTION DEVICE", "instruction": "Copying… %d of %d" % [ _job_pages - _pages_left + 1, _job_pages], "progress": 1.0 - float(_pages_left) / float(maxi(_job_pages, 1)), "footer": "E leave", } Phase.COPIER_JAM: return { "kind": "copier", "title": "MULTIFUNCTION DEVICE", "instruction": "PAPER JAM — CLEAR TRAY %s" % _jam_tray, "jam": true, "trays": TRAYS, "progress": 1.0 - float(_pages_left) / float(maxi(_job_pages, 1)), "footer": "press the tray number · E leave", } return {}