Kennungen, Kachelformen und Platzhalter für die Grafik

Jede Grafik auf dem Spielfeld bekommt eine Kennung. Art zeichnet sie im
gewählten Stil oder aus einem Bild gleichen Namens unter game/art.
ArtStyle zeichnet vorerst schlichte Platzhalter. TileShape beschreibt
die Nachbarn einer Kachel, damit Felsmassen außen rund und innen
geschlossen gezeichnet werden können. Die drei Stile Warm, Pastell und
Glimmen gibt es schon mit Namen, noch ohne eigene Zeichnungen.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-10-02 11:24:45 +02:00
parent 5e9836c64a
commit 9921aba63a
19 changed files with 574 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
class_name Art
extends RefCounted
## Every graphic on the playing field has an id. Art draws it in the chosen
## style, or from a picture whose file name is the id, so that an artist can
## replace a graphic without changing code: res://art/terrain/ground.png
## replaces the drawing of GROUND.
## The style changed, or pictures were loaded. Everything drawn by Art has to
## be drawn again.
signal changed
const GROUND := &"terrain/ground"
const SOFT_ROCK := &"terrain/soft_rock"
const HARD_ROCK := &"terrain/hard_rock"
const BEDROCK := &"terrain/bedrock"
## The cracks of a tile that took hits, drawn over the tile.
const CRACKS := &"terrain/cracks"
## The burst where a pickaxe lands.
const HIT := &"effect/hit"
const FIGURE := &"figure"
## The outline of the tile under the mouse.
const MARKER := &"marker"
## Graphics of later milestones, shown in the showcase of the style samples.
const DEAD_SOIL := &"soil/dead"
const LIVING_SOIL := &"soil/living"
const MOSS := &"plant/moss"
const VINE := &"plant/vine"
const FIBER_PLANT := &"plant/fiber"
const STONE := &"item/stone"
const GRAZER := &"animal/grazer"
const RUIN := &"ruin"
## Every id, in the order the showcase and the checks go through them.
const IDS: Array[StringName] = [
GROUND, SOFT_ROCK, HARD_ROCK, BEDROCK, CRACKS, HIT, FIGURE, MARKER,
DEAD_SOIL, LIVING_SOIL, MOSS, VINE, FIBER_PLANT, STONE, GRAZER, RUIN,
]
## The folder where pictures that replace drawings are looked for.
const OVERRIDE_DIR := "res://art"
## The styles to choose from, in the order the game offers them.
var styles: Array[ArtStyle] = [WarmStyle.new(), PastelStyle.new(), GlowStyle.new()]
## The style everything is drawn in.
var style: ArtStyle = styles[0]
var _overrides: Dictionary[StringName, Texture2D] = {}
## The keys of the styles, as LaunchOptions and the settings name them.
static func style_keys() -> Array[StringName]:
return [WarmStyle.KEY, PastelStyle.KEY, GlowStyle.KEY]
## Draws everything in the style named `key` from now on. Returns false, and
## changes nothing, if no style has that key.
func select(key: StringName) -> bool:
for candidate in styles:
if candidate.key() == key:
if candidate != style:
style = candidate
changed.emit()
return true
return false
## Switches to the style after the current one, and from the last to the
## first.
func select_next() -> void:
style = styles[(styles.find(style) + 1) % styles.size()]
changed.emit()
## Uses the picture `<directory>/<id>.png` instead of the drawing, for every
## id that has one. Pictures loaded earlier are dropped.
func load_overrides(directory: String) -> void:
_overrides.clear()
for id in IDS:
var texture := _load_texture(directory.path_join(String(id) + ".png"))
if texture != null:
_overrides[id] = texture
changed.emit()
## True if a picture replaces the drawing of `id`.
func has_override(id: StringName) -> bool:
return _overrides.has(id)
## Draws the graphic `id` into `rect` on `canvas`. Call it only while
## `canvas` draws, from its _draw(). `state` carries what the graphic shows,
## for example how far a plant has grown; see the styles for the entries.
func draw(canvas: CanvasItem, id: StringName, rect: Rect2, state: Dictionary = {}) -> void:
var texture: Texture2D = _overrides.get(id)
if texture != null:
canvas.draw_texture_rect(texture, rect, false)
else:
style.draw(canvas, id, rect, state)
static func _load_texture(path: String) -> Texture2D:
if path.begins_with("res://"):
return load(path) as Texture2D if ResourceLoader.exists(path) else null
if not FileAccess.file_exists(path):
return null
var image := Image.load_from_file(path)
return ImageTexture.create_from_image(image) if image != null else null
+1
View File
@@ -0,0 +1 @@
uid://dxmii13634y22
+192
View File
@@ -0,0 +1,192 @@
class_name ArtStyle
extends RefCounted
## One look of the playing field. Draws every graphic that Art names, with
## code. This base class draws plain placeholders; each style overrides the
## drawing of every id.
##
## The entries of `state`, by id:
## - GROUND, SOFT_ROCK, HARD_ROCK, BEDROCK, DEAD_SOIL and LIVING_SOIL:
## `solid`, a TileShape mask of the neighbours that are rock or the edge of
## the map; `living`, for LIVING_SOIL, the mask of living neighbours;
## `variant`, from TileShape.variant
## - CRACKS: `hits`, 1 or more; `variant`
## - HIT: `age`, from 0 when the pickaxe lands to 1 when the burst is over;
## `variant`
## - FIGURE: `player`, the player's number or -1; `facing`, 1.0 to the
## right or -1.0 to the left; `step`, an angle that grows while the figure
## walks; `moving`
## - MARKER: `reachable`
## - MOSS: `variant`; `time`, in seconds
## - VINE: `points`, the path of the vine; `time`
## - FIBER_PLANT: `growth`, from 0 (a seed) to 1 (grown); `time`
## - GRAZER: `hop`, from 0 to 1 during one hop; `facing`
## - RUIN: `awake`, from 0 (asleep) to 1 (awake); `time`
## - STONE: nothing
## The name of the style on the command line and in the settings. The base
## class is no style to choose and has none.
func key() -> StringName:
return &""
## The key of the style's name in the texts, or an empty text for the base
## class.
func title_key() -> String:
return ""
## Draws the graphic `id` into `rect` on `canvas`, while `canvas` draws.
func draw(canvas: CanvasItem, id: StringName, rect: Rect2, state: Dictionary) -> void:
match id:
Art.GROUND:
_ground(canvas, rect, state)
Art.SOFT_ROCK:
_soft_rock(canvas, rect, state)
Art.HARD_ROCK:
_hard_rock(canvas, rect, state)
Art.BEDROCK:
_bedrock(canvas, rect, state)
Art.CRACKS:
_cracks(canvas, rect, state)
Art.HIT:
_hit(canvas, rect, state)
Art.FIGURE:
_figure(canvas, rect, state)
Art.MARKER:
_marker(canvas, rect, state)
Art.DEAD_SOIL:
_dead_soil(canvas, rect, state)
Art.LIVING_SOIL:
_living_soil(canvas, rect, state)
Art.MOSS:
_moss(canvas, rect, state)
Art.VINE:
_vine(canvas, rect, state)
Art.FIBER_PLANT:
_fiber_plant(canvas, rect, state)
Art.STONE:
_stone(canvas, rect, state)
Art.GRAZER:
_grazer(canvas, rect, state)
Art.RUIN:
_ruin(canvas, rect, state)
_:
push_error("no drawing for the graphic %s" % id)
## The colour of the figure of `player`. Players keep their colour; -1, a
## player not in the world yet, gets the first.
func player_color(player: int) -> Color:
return Color(0.95, 0.85, 0.6) if player <= 0 else Color(0.55, 0.8, 0.95)
func _ground(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.42, 0.36, 0.28))
func _soft_rock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.56, 0.55, 0.52))
func _hard_rock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.33, 0.33, 0.37))
func _bedrock(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.12, 0.12, 0.14))
func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var hits: int = state.get("hits", 1)
var inset := rect.grow(-3.0)
for line in hits:
var part := (line + 1.0) / (hits + 1.0)
var from := Vector2(inset.position.x, inset.position.y + inset.size.y * part)
var to := Vector2(inset.end.x, inset.position.y + inset.size.y * (1.0 - part))
canvas.draw_line(from, to, Color(0.1, 0.08, 0.06, 0.8), 1.0)
func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var age: float = state.get("age", 0.0)
for chip in 4:
var direction := Vector2.RIGHT.rotated(TAU * chip / 4.0 + 0.4)
var at := rect.get_center() + direction * rect.size.x * 0.6 * age
canvas.draw_rect(Rect2(at - Vector2(1, 1), Vector2(2, 2)), Color(0.8, 0.8, 0.75, 1.0 - age))
func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var radius := rect.size.x * 0.3
canvas.draw_circle(rect.get_center(), radius + 1.0, Color(0.15, 0.1, 0.05))
canvas.draw_circle(rect.get_center(), radius, player_color(state.get("player", -1)))
func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var color := Color(0.4, 1.0, 0.4) if state.get("reachable", false) else Color(1.0, 0.4, 0.4)
canvas.draw_rect(rect, color, false, 1.0)
func _dead_soil(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.45, 0.42, 0.38))
func _living_soil(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.35, 0.5, 0.25))
func _moss(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_circle(rect.get_center(), rect.size.x * 0.3, Color(0.3, 0.65, 0.3))
func _vine(canvas: CanvasItem, _rect: Rect2, state: Dictionary) -> void:
var points: PackedVector2Array = state.get("points", PackedVector2Array())
if points.size() >= 2:
canvas.draw_polyline(points, Color(0.25, 0.6, 0.25), 2.0)
func _fiber_plant(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var growth: float = state.get("growth", 1.0)
var foot := Vector2(rect.get_center().x, rect.end.y)
canvas.draw_line(foot, foot - Vector2(0, rect.size.y * growth), Color(0.4, 0.7, 0.3), 2.0)
func _stone(canvas: CanvasItem, rect: Rect2, _state: Dictionary) -> void:
canvas.draw_circle(rect.get_center(), rect.size.x * 0.4, Color(0.6, 0.6, 0.58))
func _grazer(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
var lift := sin(PI * float(state.get("hop", 0.0))) * rect.size.y * 0.3
canvas.draw_circle(rect.get_center() - Vector2(0, lift), rect.size.x * 0.35, Color(0.92, 0.9, 0.85))
func _ruin(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
canvas.draw_rect(rect, Color(0.5, 0.5, 0.48))
var awake: float = state.get("awake", 0.0)
canvas.draw_circle(rect.get_center(), rect.size.x * 0.2, Color(1.0, 0.85, 0.3, awake))
# Helpers for the styles.
## A share from 0 to 1 that is the same for the same `variant` and `salt`.
static func share(variant: int, salt: int) -> float:
return float(TileShape.variant(Vector2i(variant & 0xffff, salt)) % 10007) / 10007.0
## A gentle swing from -1 to 1, slow and different for every `variant`.
static func sway(time: float, variant: int, speed := 1.3) -> float:
return sin(time * speed + share(variant, 7) * TAU)
## The point at `part` of the way along `points`, from 0 to 1.
static func along(points: PackedVector2Array, part: float) -> Vector2:
var total := 0.0
for i in points.size() - 1:
total += points[i].distance_to(points[i + 1])
var left := clampf(part, 0.0, 1.0) * total
for i in points.size() - 1:
var length := points[i].distance_to(points[i + 1])
if left <= length:
return points[i].lerp(points[i + 1], left / length if length > 0.0 else 0.0)
left -= length
return points[points.size() - 1]
+1
View File
@@ -0,0 +1 @@
uid://o2dqirvunvw4
+14
View File
@@ -0,0 +1,14 @@
class_name GlowStyle
extends ArtStyle
## A dim, cool world in which everything living shines and glows.
## Until it draws pixel art, it draws the placeholders of ArtStyle.
const KEY := &"glow"
func key() -> StringName:
return KEY
func title_key() -> String:
return "STYLE_GLOW"
+1
View File
@@ -0,0 +1 @@
uid://ce6e46e8mwi8f
+14
View File
@@ -0,0 +1,14 @@
class_name PastelStyle
extends ArtStyle
## Soft, light colours with outlines in a darker shade of what they surround.
## Until it draws pixel art, it draws the placeholders of ArtStyle.
const KEY := &"pastel"
func key() -> StringName:
return KEY
func title_key() -> String:
return "STYLE_PASTEL"
+1
View File
@@ -0,0 +1 @@
uid://dr2fry8j3yai8
+49
View File
@@ -0,0 +1,49 @@
class_name TileShape
extends RefCounted
## Neighbourhoods on the tile grid, so that a style can draw each tile to fit
## the tiles around it, and a stable number per tile for small variations.
## Bits of a neighbour mask, clockwise from north.
const N := 1
const NE := 2
const E := 4
const SE := 8
const S := 16
const SW := 32
const W := 64
const NW := 128
## Where the neighbour of each bit lies, in the order of the bits.
const OFFSETS: Array[Vector2i] = [
Vector2i(0, -1), Vector2i(1, -1), Vector2i(1, 0), Vector2i(1, 1),
Vector2i(0, 1), Vector2i(-1, 1), Vector2i(-1, 0), Vector2i(-1, -1),
]
## One byte per tile of `codes`: 1 if `members` holds its code, else 0.
static func flags(codes: PackedByteArray, members: Array[int]) -> PackedByteArray:
var result := PackedByteArray()
result.resize(codes.size())
for index in codes.size():
result[index] = 1 if members.has(codes[index]) else 0
return result
## The neighbours of `tile` whose flag is 1, as a mask of the bits above, on
## a map of `width` by `height` tiles. Beyond the edge of the map, every
## neighbour counts as set if `outside_set` is true.
static func mask(tile_flags: PackedByteArray, width: int, height: int, tile: Vector2i, outside_set: bool) -> int:
var result := 0
for bit in 8:
var at := tile + OFFSETS[bit]
var inside := at.x >= 0 and at.y >= 0 and at.x < width and at.y < height
if (inside and tile_flags[at.y * width + at.x] == 1) or (not inside and outside_set):
result |= 1 << bit
return result
## A number from 0 to 2147483647 that is the same for `tile` every time and
## looks random from one tile to the next.
static func variant(tile: Vector2i) -> int:
var h := (tile.x * 374761393 + tile.y * 668265263) & 0x7fffffff
h = ((h ^ (h >> 13)) * 1274126177) & 0x7fffffff
return h ^ (h >> 16)
+1
View File
@@ -0,0 +1 @@
uid://dfj1ix7h3wky2
+14
View File
@@ -0,0 +1,14 @@
class_name WarmStyle
extends ArtStyle
## Warm, full colours and dark outlines, close to the classic farming games.
## Until it draws pixel art, it draws the placeholders of ArtStyle.
const KEY := &"warm"
func key() -> StringName:
return KEY
func title_key() -> String:
return "STYLE_WARM"
+1
View File
@@ -0,0 +1 @@
uid://7kb2jlqpvvoe