## Rock entity — mineable by a pawn with a Mine job. Drops a stone Item node ## when mined out. ## ## Mirrors Tree's chopping model; stone is harder so MINE_TICKS is longer. ## A MineProvider (Opus, Phase 4) creates a Job whose INTERACT toil calls ## on_mine_tick() once per sim tick via JobRunner. ## ## World registration (World.register_rock / World.unregister_rock) is called ## here but the methods land in World during Opus integration. class_name Rock extends Node2D const TILE_SIZE_PX: int = 16 ## Sim ticks to mine a rock at 1× speed (120 ticks = 6 sim seconds at 20 Hz). ## Stone is harder than wood — MINE_TICKS > Tree.CHOP_TICKS. const MINE_TICKS: int = 120 ## Stone Items dropped on a successful mine. const STONE_DROPS_ON_MINE: int = 1 # ── state ───────────────────────────────────────────────────────────────────── var tile: Vector2i = Vector2i.ZERO ## 0..MINE_TICKS. Advanced by on_mine_tick(); rock is mined when equal to MINE_TICKS. var mine_progress: int = 0 ## True once a player has painted a mine designation on this rock. MineProvider ## ignores undesignated rocks (Rimworld parity — pawns don't auto-mine). var mine_designated: bool = false # Preloaded scene for spawned stone items. const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn") ## Rock sprite atlas — re-uses the Grasslands tileset (already imported for the ## decoration overlay). These coords pick standalone single-tile boulders from ## the rock band (x=16..29, y=3 and y=5) — chosen because each has a clean ## green margin on all four sides, so they read as separate small rocks rather ## than chunks of a tiled cluster. The earlier (24,7)/(28,7)/(12,13) coords ## were autotile interior pieces; replaced 2026-05-12 after the user flagged ## the obvious tile-cluster artifacts on small rocks. ## ## Multi-tile cluster formations live at (22..23, 3..4) brown and (30..31, 3..4) ## gray — those are reserved for the upcoming BigRock entity (2×2 boulder). const _ROCK_TEX: Texture2D = preload("res://art/tiles/FG_Grasslands_Spring.png") const _ROCK_VARIANT_COORDS: Array[Vector2i] = [ Vector2i(16, 3), # brown round, medium Vector2i(20, 3), # brown peaked, smaller Vector2i(16, 5), # brown squat, low Vector2i(24, 3), # gray round, medium Vector2i(28, 3), # gray peaked, smaller Vector2i(24, 5), # gray squat, low ] # ── lifecycle ───────────────────────────────────────────────────────────────── func _ready() -> void: position = _tile_to_world(tile) _build_sprite() World.register_rock(self) ## Adds a Sprite2D child with one of the rock variants. Variant chosen ## deterministically from the tile coord so the same tile renders the same ## rock across boots and load/save. func _build_sprite() -> void: var sprite := Sprite2D.new() sprite.name = "Sprite" sprite.texture = _ROCK_TEX sprite.region_enabled = true var coord: Vector2i = _ROCK_VARIANT_COORDS[(tile.x * 31 + tile.y * 17) % _ROCK_VARIANT_COORDS.size()] sprite.region_rect = Rect2(coord.x * TILE_SIZE_PX, coord.y * TILE_SIZE_PX, TILE_SIZE_PX, TILE_SIZE_PX) sprite.centered = true sprite.offset = Vector2.ZERO # 16×16 tile, sits centered on the tile add_child(sprite) func _exit_tree() -> void: World.unregister_rock(self) # ── public API ──────────────────────────────────────────────────────────────── ## One-shot initialiser. Call after add_child() so _ready() already fired. func setup(start_tile: Vector2i) -> void: tile = start_tile mine_progress = 0 position = _tile_to_world(tile) queue_redraw() Audit.log("rock", "spawned at %s" % tile) ## True when the rock hasn't been fully mined yet. func is_mineable() -> bool: return mine_progress < MINE_TICKS ## Walk destination for a pawn approaching this rock. Small rocks are walkable ## (Phase 4 simplification), so the pawn stands on the rock tile while mining. ## BigRock overrides this to return a perimeter neighbour because its footprint ## is blocked. MineProvider always asks the entity for this rather than reading ## `.tile` directly, so single Rock and BigRock plug into the same walk toil. func approach_tile_for(_pawn_tile: Vector2i) -> Vector2i: return tile ## Called by the INTERACT toil in JobRunner once per sim tick while the pawn ## works this rock. Advances mine_progress and triggers mined() when complete. func on_mine_tick() -> void: if not is_mineable(): return mine_progress += 1 queue_redraw() if mine_progress >= MINE_TICKS: mined() ## Drop stone Item(s) and free this node. Called automatically by on_mine_tick() ## but also accessible for scripted removal (debug, storyteller events). func mined() -> void: # Apply mine buff (veins_of_iron): multiply stone drops with stochastic rounding. var drop_count: int = Storyteller.multiply_drops(STONE_DROPS_ON_MINE, Storyteller.get_buff_multiplier(&"mine")) var item: Item = ITEM_SCENE.instantiate() get_parent().add_child(item) item.setup(Item.TYPE_STONE, drop_count, tile) Audit.log("rock", "mined at %s; %d stone drop(s) (buff mult=%.2f)" % [tile, drop_count, Storyteller.get_buff_multiplier(&"mine")]) if Audio != null: Audio.play_sfx(&"mine_tick") World.clear_designation_at(tile) queue_free() # ── save / load ─────────────────────────────────────────────────────────────── func to_dict() -> Dictionary: return { "class_id": &"rock", "tile_x": tile.x, "tile_y": tile.y, "mine_progress": mine_progress, "mine_designated": mine_designated, } static func from_dict(d: Dictionary) -> Dictionary: return { "tile_x": int(d.get("tile_x", 0)), "tile_y": int(d.get("tile_y", 0)), "mine_progress": int(d.get("mine_progress", 0)), "mine_designated": bool(d.get("mine_designated", false)), } # ── render ──────────────────────────────────────────────────────────────────── func _draw() -> void: # Rock body comes from the Sprite2D child (see _build_sprite). # This _draw renders only the mine-progress crack overlaid on the sprite. if mine_progress > 0: var ratio := float(mine_progress) / float(MINE_TICKS) var crack_len := ratio * 5.0 draw_line( Vector2(-1.0, -2.0), Vector2(-1.0 + crack_len, 1.0), Color(0.15, 0.12, 0.10, 0.85), 1.5 ) # ── helpers ─────────────────────────────────────────────────────────────────── func _tile_to_world(t: Vector2i) -> Vector2: return Vector2( t.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0, t.y * TILE_SIZE_PX + TILE_SIZE_PX / 2.0 )