M1d: Stilproben #12
@@ -19,3 +19,5 @@ window/size/viewport_height=720
|
|||||||
|
|
||||||
renderer/rendering_method="gl_compatibility"
|
renderer/rendering_method="gl_compatibility"
|
||||||
renderer/rendering_method.mobile="gl_compatibility"
|
renderer/rendering_method.mobile="gl_compatibility"
|
||||||
|
textures/canvas_textures/default_texture_filter=0
|
||||||
|
2d/snap/snap_2d_transforms_to_pixel=true
|
||||||
|
|||||||
@@ -0,0 +1,471 @@
|
|||||||
|
class_name PixelStyle
|
||||||
|
extends ArtStyle
|
||||||
|
## Pixel art in the manner of classic farming games, drawn in code. A tile
|
||||||
|
## is 16 by 16 pixels, seen a little from the front: rock shows its face
|
||||||
|
## toward open ground, and figures, animals and plants stand upright. Every
|
||||||
|
## graphic is made once as a small image, pixel by pixel from the palette,
|
||||||
|
## and kept; the camera enlarges each pixel to a crisp square.
|
||||||
|
##
|
||||||
|
## The subclasses choose the palette and the outlines.
|
||||||
|
|
||||||
|
## Pixels along the edge of a tile.
|
||||||
|
const TILE := 16
|
||||||
|
## Rows of the face that rock shows toward open ground below it.
|
||||||
|
const FACE := 6
|
||||||
|
## Images of a burst of chips, from the hit to its end.
|
||||||
|
const HIT_FRAMES := 6
|
||||||
|
## Size of a figure: a tile wide and one and a half tiles tall.
|
||||||
|
const FIGURE_SIZE := Vector2i(16, 24)
|
||||||
|
const CLEAR := Color(0, 0, 0, 0)
|
||||||
|
|
||||||
|
## Colours by name; the subclass fills them.
|
||||||
|
var palette: Dictionary[String, Color] = {}
|
||||||
|
## The colours of the players, in the order of their numbers.
|
||||||
|
var players: Array[Color] = []
|
||||||
|
## True for outlines in a darker shade of what they surround, false for one
|
||||||
|
## dark outline colour.
|
||||||
|
var tinted_outlines := false
|
||||||
|
var _textures: Dictionary[String, Texture2D] = {}
|
||||||
|
|
||||||
|
|
||||||
|
## A small image that the drawings put their pixels on.
|
||||||
|
class Pixels:
|
||||||
|
const SIDES: Array[Vector2i] = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]
|
||||||
|
const CLEAR := Color(0, 0, 0, 0)
|
||||||
|
|
||||||
|
var image: Image
|
||||||
|
var size: Vector2i
|
||||||
|
|
||||||
|
func _init(width: int, height: int) -> void:
|
||||||
|
size = Vector2i(width, height)
|
||||||
|
image = Image.create_empty(width, height, false, Image.FORMAT_RGBA8)
|
||||||
|
|
||||||
|
func has(x: int, y: int) -> bool:
|
||||||
|
return x >= 0 and y >= 0 and x < size.x and y < size.y
|
||||||
|
|
||||||
|
func put(x: int, y: int, color: Color) -> void:
|
||||||
|
if has(x, y):
|
||||||
|
image.set_pixel(x, y, color)
|
||||||
|
|
||||||
|
## The colour at x, y, transparent outside the image.
|
||||||
|
func at(x: int, y: int) -> Color:
|
||||||
|
return image.get_pixel(x, y) if has(x, y) else CLEAR
|
||||||
|
|
||||||
|
func filled(x: int, y: int) -> bool:
|
||||||
|
return at(x, y).a > 0.0
|
||||||
|
|
||||||
|
## Lays the translucent `color` over the pixel at x, y.
|
||||||
|
func shade(x: int, y: int, color: Color) -> void:
|
||||||
|
if has(x, y):
|
||||||
|
image.set_pixel(x, y, image.get_pixel(x, y).blend(color))
|
||||||
|
|
||||||
|
func rect(x: int, y: int, width: int, height: int, color: Color) -> void:
|
||||||
|
for row in range(y, y + height):
|
||||||
|
for column in range(x, x + width):
|
||||||
|
put(column, row, color)
|
||||||
|
|
||||||
|
## Fills the ellipse around cx, cy with the half axes rx and ry.
|
||||||
|
func ellipse(cx: float, cy: float, rx: float, ry: float, color: Color) -> void:
|
||||||
|
for row in range(floori(cy - ry), ceili(cy + ry) + 1):
|
||||||
|
for column in range(floori(cx - rx), ceili(cx + rx) + 1):
|
||||||
|
var dx := (column + 0.5 - cx) / rx
|
||||||
|
var dy := (row + 0.5 - cy) / ry
|
||||||
|
if dx * dx + dy * dy <= 1.0:
|
||||||
|
put(column, row, color)
|
||||||
|
|
||||||
|
## A line of single pixels from x0, y0 to x1, y1.
|
||||||
|
func line(x0: int, y0: int, x1: int, y1: int, color: Color) -> void:
|
||||||
|
var dx := absi(x1 - x0)
|
||||||
|
var dy := -absi(y1 - y0)
|
||||||
|
var sx := 1 if x0 < x1 else -1
|
||||||
|
var sy := 1 if y0 < y1 else -1
|
||||||
|
var error := dx + dy
|
||||||
|
var x := x0
|
||||||
|
var y := y0
|
||||||
|
while true:
|
||||||
|
put(x, y, color)
|
||||||
|
if x == x1 and y == y1:
|
||||||
|
return
|
||||||
|
var twice := 2 * error
|
||||||
|
if twice >= dy:
|
||||||
|
error += dy
|
||||||
|
x += sx
|
||||||
|
if twice <= dx:
|
||||||
|
error += dx
|
||||||
|
y += sy
|
||||||
|
|
||||||
|
## Surrounds everything solid drawn so far with a line one pixel wide: in
|
||||||
|
## `color`, or, with `tinted`, in a darker shade of the pixel beside it.
|
||||||
|
func outline(color: Color, tinted: bool) -> void:
|
||||||
|
var found: Array[Vector3i] = []
|
||||||
|
var colors: Array[Color] = []
|
||||||
|
for y in size.y:
|
||||||
|
for x in size.x:
|
||||||
|
if filled(x, y):
|
||||||
|
continue
|
||||||
|
for side in SIDES:
|
||||||
|
var beside := at(x + side.x, y + side.y)
|
||||||
|
if beside.a >= 1.0:
|
||||||
|
found.append(Vector3i(x, y, 0))
|
||||||
|
colors.append(beside.darkened(0.5) if tinted else color)
|
||||||
|
break
|
||||||
|
for index in found.size():
|
||||||
|
put(found[index].x, found[index].y, colors[index])
|
||||||
|
|
||||||
|
|
||||||
|
func player_color(player: int) -> Color:
|
||||||
|
return players[maxi(0, player) % players.size()]
|
||||||
|
|
||||||
|
|
||||||
|
func _c(name: String) -> Color:
|
||||||
|
return palette[name]
|
||||||
|
|
||||||
|
|
||||||
|
## The dark line around things, or around a thing of colour `fill` when
|
||||||
|
## outlines are tinted.
|
||||||
|
func _ink(fill: Color) -> Color:
|
||||||
|
return fill.darkened(0.5) if tinted_outlines else _c("outline")
|
||||||
|
|
||||||
|
|
||||||
|
## A number from 0 to 1 that looks random and is the same for the same
|
||||||
|
## arguments.
|
||||||
|
static func _noise(x: int, y: int, salt: int) -> float:
|
||||||
|
return float(TileShape.variant(Vector2i(x * 31 + salt * 977, y * 17 - salt * 131)) % 1000) / 1000.0
|
||||||
|
|
||||||
|
|
||||||
|
## The texture made by `make` under `key`, made only the first time.
|
||||||
|
func _image(key: String, make: Callable) -> Texture2D:
|
||||||
|
var texture: Texture2D = _textures.get(key)
|
||||||
|
if texture == null:
|
||||||
|
var pixels: Pixels = make.call()
|
||||||
|
texture = ImageTexture.create_from_image(pixels.image)
|
||||||
|
_textures[key] = texture
|
||||||
|
return texture
|
||||||
|
|
||||||
|
|
||||||
|
## Draws `texture` with its top left corner at `at`, on whole pixels.
|
||||||
|
static func _put(canvas: CanvasItem, texture: Texture2D, at: Vector2, modulate := Color.WHITE) -> void:
|
||||||
|
canvas.draw_texture(texture, at.round(), modulate)
|
||||||
|
|
||||||
|
|
||||||
|
## The neighbours of `mask` that change how a tile looks: the four sides,
|
||||||
|
## and a corner only where both sides beside it are set.
|
||||||
|
static func _shape(mask: int) -> int:
|
||||||
|
var result := mask & (TileShape.N | TileShape.E | TileShape.S | TileShape.W)
|
||||||
|
var corners := [
|
||||||
|
[TileShape.NE, TileShape.N | TileShape.E], [TileShape.SE, TileShape.S | TileShape.E],
|
||||||
|
[TileShape.SW, TileShape.S | TileShape.W], [TileShape.NW, TileShape.N | TileShape.W],
|
||||||
|
]
|
||||||
|
for corner: Array in corners:
|
||||||
|
var sides: int = corner[1]
|
||||||
|
if (mask & corner[0]) != 0 and (mask & sides) == sides:
|
||||||
|
result |= corner[0]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Terrain.
|
||||||
|
|
||||||
|
|
||||||
|
func _ground(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
||||||
|
var shadow: int = state.get("solid", 0) & (TileShape.N | TileShape.W | TileShape.NW)
|
||||||
|
var variant: int = state.get("variant", 0) % 8
|
||||||
|
_put(canvas, _image("ground/%d/%d" % [shadow, variant], func() -> Pixels: return _earth(variant, shadow)), rect.position)
|
||||||
|
|
||||||
|
|
||||||
|
## The bare earth of the dead world: up to two soft patches of darker earth,
|
||||||
|
## a light grain or two, now and then a pebble.
|
||||||
|
func _earth(variant: int, shadow: int) -> Pixels:
|
||||||
|
var pixels := Pixels.new(TILE, TILE)
|
||||||
|
pixels.rect(0, 0, TILE, TILE, _c("ground"))
|
||||||
|
var patches: int = [0, 1, 2, 1, 0, 1, 1, 2][variant]
|
||||||
|
for patch in patches:
|
||||||
|
var at := _spot(patch, variant, 2, 11)
|
||||||
|
for dy in 2:
|
||||||
|
for dx in range(-1, 2):
|
||||||
|
if _noise(at.x + dx, at.y + dy, variant + 40) < 0.6:
|
||||||
|
pixels.put(at.x + dx, at.y + dy, _c("ground_dark"))
|
||||||
|
for grain in 1 + variant % 2:
|
||||||
|
var at := _spot(grain + 4, variant, 1, 14)
|
||||||
|
pixels.put(at.x, at.y, _c("ground_light"))
|
||||||
|
if variant % 4 == 1:
|
||||||
|
var at := _spot(9, variant, 3, 9)
|
||||||
|
_pebble(pixels, at.x, at.y)
|
||||||
|
_cast_shadow(pixels, shadow)
|
||||||
|
return pixels
|
||||||
|
|
||||||
|
|
||||||
|
## A point in the tile that is the same for the same `index` and `variant`,
|
||||||
|
## from `low` to `low + span - 1` on both axes.
|
||||||
|
static func _spot(index: int, variant: int, low: int, span: int) -> Vector2i:
|
||||||
|
return Vector2i(low + int(_noise(index, 1, variant) * span), low + int(_noise(index, 2, variant) * span))
|
||||||
|
|
||||||
|
|
||||||
|
func _pebble(pixels: Pixels, x: int, y: int) -> void:
|
||||||
|
pixels.put(x, y, _c("pebble_light"))
|
||||||
|
pixels.put(x + 1, y, _c("pebble"))
|
||||||
|
pixels.put(x, y + 1, _c("pebble"))
|
||||||
|
pixels.put(x + 1, y + 1, _c("pebble_dark"))
|
||||||
|
|
||||||
|
|
||||||
|
## Darkens the ground where rock to the north or west casts its shadow; the
|
||||||
|
## light falls from the top left.
|
||||||
|
func _cast_shadow(pixels: Pixels, shadow: int) -> void:
|
||||||
|
var deep := Color(0, 0, 0, 0.32)
|
||||||
|
var faint := Color(0, 0, 0, 0.16)
|
||||||
|
if (shadow & TileShape.N) != 0:
|
||||||
|
for x in TILE:
|
||||||
|
pixels.shade(x, 0, deep)
|
||||||
|
pixels.shade(x, 1, deep if x % 2 == 0 else faint)
|
||||||
|
pixels.shade(x, 2, faint if x % 2 == 1 else CLEAR)
|
||||||
|
if (shadow & TileShape.W) != 0:
|
||||||
|
for y in range(1 if (shadow & TileShape.N) != 0 else 0, TILE):
|
||||||
|
pixels.shade(0, y, faint)
|
||||||
|
elif (shadow & (TileShape.N | TileShape.NW)) == TileShape.NW:
|
||||||
|
pixels.shade(0, 0, deep)
|
||||||
|
|
||||||
|
|
||||||
|
func _soft_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
||||||
|
_rock(canvas, rect, state, "soft")
|
||||||
|
|
||||||
|
|
||||||
|
func _hard_rock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
||||||
|
_rock(canvas, rect, state, "hard")
|
||||||
|
|
||||||
|
|
||||||
|
func _bedrock(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
||||||
|
_rock(canvas, rect, state, "bed")
|
||||||
|
|
||||||
|
|
||||||
|
## Rock of `kind`: its top, and its face where the ground below is open.
|
||||||
|
## The ground shows where the outer corners are round.
|
||||||
|
func _rock(canvas: CanvasItem, rect: Rect2, state: Dictionary, kind: String) -> void:
|
||||||
|
var mask := _shape(state.get("solid", 0))
|
||||||
|
var variant: int = state.get("variant", 0) % 4
|
||||||
|
_ground(canvas, rect, {"variant": state.get("variant", 0)})
|
||||||
|
var key := "%s/%d/%d" % [kind, mask, variant]
|
||||||
|
_put(canvas, _image(key, func() -> Pixels: return _rock_image(kind, mask, variant)), rect.position)
|
||||||
|
|
||||||
|
|
||||||
|
func _rock_image(kind: String, mask: int, variant: int) -> Pixels:
|
||||||
|
var open_n := (mask & TileShape.N) == 0
|
||||||
|
var open_e := (mask & TileShape.E) == 0
|
||||||
|
var open_s := (mask & TileShape.S) == 0
|
||||||
|
var open_w := (mask & TileShape.W) == 0
|
||||||
|
var top_rows := TILE - FACE if open_s else TILE
|
||||||
|
var ink := _c("outline")
|
||||||
|
var pixels := Pixels.new(TILE, TILE)
|
||||||
|
for y in TILE:
|
||||||
|
for x in TILE:
|
||||||
|
var color := _c(kind + "_top")
|
||||||
|
if y >= top_rows:
|
||||||
|
color = _c(kind + "_face")
|
||||||
|
if y == top_rows or ((x + variant) % 4 == 0 and y < TILE - 2):
|
||||||
|
color = _c(kind + "_face_dark")
|
||||||
|
else:
|
||||||
|
if (open_n and y == 1) or (open_w and x == 1):
|
||||||
|
color = _c(kind + "_top_light")
|
||||||
|
elif open_e and x == TILE - 2:
|
||||||
|
color = _c(kind + "_top_dark")
|
||||||
|
if open_s and y == top_rows - 1:
|
||||||
|
color = _c(kind + "_lip")
|
||||||
|
pixels.put(x, y, color)
|
||||||
|
_rock_marks(pixels, kind, variant, top_rows)
|
||||||
|
if open_n:
|
||||||
|
pixels.rect(0, 0, TILE, 1, ink)
|
||||||
|
if open_w:
|
||||||
|
pixels.rect(0, 0, 1, TILE, ink)
|
||||||
|
if open_e:
|
||||||
|
pixels.rect(TILE - 1, 0, 1, TILE, ink)
|
||||||
|
if open_s:
|
||||||
|
pixels.rect(0, TILE - 1, TILE, 1, ink)
|
||||||
|
# Inner corners, where the mass turns around open ground.
|
||||||
|
if not open_n and not open_w and (mask & TileShape.NW) == 0:
|
||||||
|
pixels.put(0, 0, ink)
|
||||||
|
if not open_n and not open_e and (mask & TileShape.NE) == 0:
|
||||||
|
pixels.put(TILE - 1, 0, ink)
|
||||||
|
if not open_s and not open_w and (mask & TileShape.SW) == 0:
|
||||||
|
pixels.rect(0, TILE - FACE, 1, FACE, ink)
|
||||||
|
if not open_s and not open_e and (mask & TileShape.SE) == 0:
|
||||||
|
pixels.rect(TILE - 1, TILE - FACE, 1, FACE, ink)
|
||||||
|
# Round outer corners.
|
||||||
|
if open_n and open_w:
|
||||||
|
_round(pixels, Vector2i(0, 0), Vector2i(1, 1), ink)
|
||||||
|
if open_n and open_e:
|
||||||
|
_round(pixels, Vector2i(TILE - 1, 0), Vector2i(-1, 1), ink)
|
||||||
|
if open_s and open_w:
|
||||||
|
_round(pixels, Vector2i(0, TILE - 1), Vector2i(1, -1), ink)
|
||||||
|
if open_s and open_e:
|
||||||
|
_round(pixels, Vector2i(TILE - 1, TILE - 1), Vector2i(-1, -1), ink)
|
||||||
|
return pixels
|
||||||
|
|
||||||
|
|
||||||
|
## Cuts the corner at `corner` and closes the outline one pixel further in,
|
||||||
|
## in the direction `inward`.
|
||||||
|
static func _round(pixels: Pixels, corner: Vector2i, inward: Vector2i, ink: Color) -> void:
|
||||||
|
pixels.put(corner.x, corner.y, CLEAR)
|
||||||
|
pixels.put(corner.x + inward.x, corner.y, CLEAR)
|
||||||
|
pixels.put(corner.x, corner.y + inward.y, CLEAR)
|
||||||
|
pixels.put(corner.x + inward.x, corner.y + inward.y, ink)
|
||||||
|
|
||||||
|
|
||||||
|
## Small details on the top of rock: pores and chips on soft rock, glinting
|
||||||
|
## crystals in hard rock, layers in bedrock.
|
||||||
|
func _rock_marks(pixels: Pixels, kind: String, variant: int, top_rows: int) -> void:
|
||||||
|
var room := maxi(1, top_rows - 6)
|
||||||
|
for pore in 2:
|
||||||
|
var spot := Vector2i(3 + int(_noise(pore, 8, variant) * 10.0), 3 + int(_noise(pore, 9, variant) * room))
|
||||||
|
pixels.put(spot.x, spot.y, _c(kind + "_top_dark"))
|
||||||
|
var at := Vector2i(4 + int(_noise(7, 8, variant) * 7.0), 3 + int(_noise(7, 9, variant) * room))
|
||||||
|
match kind:
|
||||||
|
"soft":
|
||||||
|
if variant % 2 == 0:
|
||||||
|
pixels.put(at.x, at.y, _c("soft_top_dark"))
|
||||||
|
pixels.put(at.x + 1, at.y, _c("soft_top_dark"))
|
||||||
|
pixels.put(at.x + 1, at.y - 1, _c("soft_top_light"))
|
||||||
|
pixels.put(at.x + 2, at.y + 1, _c("soft_top_dark"))
|
||||||
|
"hard":
|
||||||
|
if variant % 2 == 1:
|
||||||
|
pixels.put(at.x, at.y, _c("hard_glint"))
|
||||||
|
pixels.put(at.x + 1, at.y, _c("hard_top_light"))
|
||||||
|
pixels.put(at.x, at.y + 1, _c("hard_top_light"))
|
||||||
|
"bed":
|
||||||
|
for row in range(4, top_rows - 1, 5):
|
||||||
|
for column in range(3, TILE - 3):
|
||||||
|
if _noise(column, row, variant) < 0.5:
|
||||||
|
pixels.put(column, row, _c("bed_top_dark"))
|
||||||
|
|
||||||
|
|
||||||
|
func _cracks(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
||||||
|
var hits := clampi(state.get("hits", 1), 1, 3)
|
||||||
|
var variant: int = state.get("variant", 0) % 4
|
||||||
|
_put(canvas, _image("cracks/%d/%d" % [hits, variant], func() -> Pixels: return _crack_image(hits, variant)), rect.position)
|
||||||
|
|
||||||
|
|
||||||
|
func _crack_image(hits: int, variant: int) -> Pixels:
|
||||||
|
var pixels := Pixels.new(TILE, TILE)
|
||||||
|
var path: Array[Vector2i] = []
|
||||||
|
var at := Vector2i(5 + variant, 3)
|
||||||
|
for step in 2 + hits * 3:
|
||||||
|
path.append(at)
|
||||||
|
var turn := _noise(step, hits, variant)
|
||||||
|
at += Vector2i(-1 if turn < 0.3 else (1 if turn > 0.65 else 0), 1)
|
||||||
|
if hits >= 2:
|
||||||
|
var branch: Vector2i = path[path.size() >> 1]
|
||||||
|
for step in 3:
|
||||||
|
branch += Vector2i(1, 1 if step % 2 == 0 else 0)
|
||||||
|
path.append(branch)
|
||||||
|
for point in path:
|
||||||
|
pixels.put(point.x + 1, point.y, _c("soft_top_light"))
|
||||||
|
for point in path:
|
||||||
|
pixels.put(point.x, point.y, _c("outline"))
|
||||||
|
return pixels
|
||||||
|
|
||||||
|
|
||||||
|
func _hit(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
||||||
|
var frame := clampi(int(float(state.get("age", 0.0)) * HIT_FRAMES), 0, HIT_FRAMES - 1)
|
||||||
|
var variant: int = state.get("variant", 0) % 4
|
||||||
|
_put(canvas, _image("hit/%d/%d" % [frame, variant], func() -> Pixels: return _hit_image(frame, variant)), rect.position - Vector2(8, 8))
|
||||||
|
|
||||||
|
|
||||||
|
## Chips that fly up and fall, and a puff of dust, in a square of two tiles
|
||||||
|
## around the tile.
|
||||||
|
func _hit_image(frame: int, variant: int) -> Pixels:
|
||||||
|
var pixels := Pixels.new(TILE * 2, TILE * 2)
|
||||||
|
var t := (frame + 1.0) / HIT_FRAMES
|
||||||
|
for chip in 5:
|
||||||
|
var angle := -PI / 2.0 + (chip - 2) * 0.6 + (variant - 1.5) * 0.15
|
||||||
|
var distance := 3.0 + t * 10.0
|
||||||
|
var x := roundi(TILE + cos(angle) * distance)
|
||||||
|
var y := roundi(TILE - 1 + sin(angle) * distance + t * t * 10.0)
|
||||||
|
pixels.put(x, y, _c("soft_top_light"))
|
||||||
|
if frame < 3 and chip % 2 == 0:
|
||||||
|
pixels.put(x + 1, y, _c("soft_top"))
|
||||||
|
pixels.put(x, y + 1, _c("soft_face"))
|
||||||
|
pixels.put(x + 1, y + 1, _c("soft_face_dark"))
|
||||||
|
if frame < 3:
|
||||||
|
pixels.outline(_c("outline"), tinted_outlines)
|
||||||
|
var radius := 3.0 + frame * 2.0
|
||||||
|
for puff in 8:
|
||||||
|
var angle := TAU * puff / 8.0 + variant
|
||||||
|
var dust := _c("dust")
|
||||||
|
dust.a = 0.75 - frame * 0.2
|
||||||
|
pixels.put(roundi(TILE + cos(angle) * radius), roundi(TILE - 1 + sin(angle) * radius * 0.6), dust)
|
||||||
|
return pixels
|
||||||
|
|
||||||
|
|
||||||
|
func _marker(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
||||||
|
var reachable: bool = state.get("reachable", false)
|
||||||
|
_put(canvas, _image("marker/%s" % reachable, func() -> Pixels: return _marker_image(reachable)), rect.position - Vector2(1, 1))
|
||||||
|
|
||||||
|
|
||||||
|
## Four corners around the tile, with a dark rim that shows on any ground.
|
||||||
|
func _marker_image(reachable: bool) -> Pixels:
|
||||||
|
var pixels := Pixels.new(TILE + 2, TILE + 2)
|
||||||
|
var color := _c("marker_near") if reachable else _c("marker_far")
|
||||||
|
var last := TILE
|
||||||
|
for corner: Vector2i in [Vector2i(1, 1), Vector2i(last, 1), Vector2i(1, last), Vector2i(last, last)]:
|
||||||
|
var dx := 1 if corner.x == 1 else -1
|
||||||
|
var dy := 1 if corner.y == 1 else -1
|
||||||
|
for i in 4:
|
||||||
|
pixels.put(corner.x + dx * i, corner.y, color)
|
||||||
|
pixels.put(corner.x, corner.y + dy * i, color)
|
||||||
|
pixels.outline(_c("outline"), false)
|
||||||
|
return pixels
|
||||||
|
|
||||||
|
|
||||||
|
# Figures.
|
||||||
|
|
||||||
|
|
||||||
|
func _figure(canvas: CanvasItem, rect: Rect2, state: Dictionary) -> void:
|
||||||
|
var frame := 0
|
||||||
|
if state.get("moving", false):
|
||||||
|
frame = 1 + int(fposmod(float(state.get("step", 0.0)), TAU) / TAU * 4.0) % 4
|
||||||
|
var colour := maxi(0, state.get("player", -1)) % players.size()
|
||||||
|
var left: bool = float(state.get("facing", 1.0)) < 0.0
|
||||||
|
var foot := Vector2(rect.get_center().x, rect.end.y)
|
||||||
|
_put(canvas, _image("shadow/12", func() -> Pixels: return _shadow_image(12)), foot - Vector2(6, 2))
|
||||||
|
var key := "figure/%d/%d/%s" % [colour, frame, left]
|
||||||
|
_put(canvas, _image(key, func() -> Pixels: return _figure_image(colour, frame, left)), foot - Vector2(FIGURE_SIZE.x / 2.0, FIGURE_SIZE.y))
|
||||||
|
|
||||||
|
|
||||||
|
## A soft shadow on the ground, `width` pixels wide.
|
||||||
|
func _shadow_image(width: int) -> Pixels:
|
||||||
|
var pixels := Pixels.new(width, 3)
|
||||||
|
pixels.ellipse(width / 2.0, 1.5, width / 2.0, 1.5, Color(0, 0, 0, 0.28))
|
||||||
|
return pixels
|
||||||
|
|
||||||
|
|
||||||
|
## A figure in walking frame `frame` (0 standing, 1 to 4 walking), facing
|
||||||
|
## right or, with `left`, left. The shirt has the colour of the player.
|
||||||
|
func _figure_image(colour: int, frame: int, left: bool) -> Pixels:
|
||||||
|
var pixels := Pixels.new(FIGURE_SIZE.x, FIGURE_SIZE.y)
|
||||||
|
var shirt := players[colour]
|
||||||
|
var bob := -1 if frame == 2 or frame == 4 else 0
|
||||||
|
# The far leg and the near leg, by frame.
|
||||||
|
var legs: Array[Vector2i] = [Vector2i(6, 9), Vector2i(5, 10), Vector2i(7, 8), Vector2i(10, 5), Vector2i(8, 7)]
|
||||||
|
for leg in 2:
|
||||||
|
var x := legs[frame].x if leg == 0 else legs[frame].y
|
||||||
|
var lifted := 1 if frame > 0 and frame % 2 == 1 and leg == 0 else 0
|
||||||
|
pixels.rect(x, 18, 2, 4 - lifted, _c("pants_dark") if leg == 0 else _c("pants"))
|
||||||
|
pixels.rect(x, 22 - lifted, 3, 2, _c("boots"))
|
||||||
|
pixels.rect(5, 11 + bob, 6, 8, shirt)
|
||||||
|
pixels.rect(5, 11 + bob, 1, 8, shirt.darkened(0.2))
|
||||||
|
pixels.rect(5, 18 + bob, 6, 1, shirt.darkened(0.35))
|
||||||
|
var swing: int = [0, 1, 0, -1, 0][frame]
|
||||||
|
pixels.rect(8 + swing, 12 + bob, 2, 5, shirt.darkened(0.15))
|
||||||
|
pixels.rect(8 + swing, 17 + bob, 2, 1, _c("skin"))
|
||||||
|
# The head: hair behind, the face in front, bangs above the eye.
|
||||||
|
pixels.ellipse(7.8, 5.8 + bob, 5.2, 5.0, _c("hair"))
|
||||||
|
pixels.ellipse(9.6, 7.4 + bob, 3.7, 3.9, _c("skin"))
|
||||||
|
pixels.rect(9, 3 + bob, 4, 2, _c("hair"))
|
||||||
|
pixels.put(6, 2 + bob, _c("hair_light"))
|
||||||
|
pixels.put(7, 2 + bob, _c("hair_light"))
|
||||||
|
pixels.put(8, 1 + bob, _c("hair_light"))
|
||||||
|
pixels.put(11, 6 + bob, _c("eye"))
|
||||||
|
pixels.put(11, 7 + bob, _c("eye"))
|
||||||
|
pixels.put(8, 8 + bob, _c("skin_shade"))
|
||||||
|
pixels.put(12, 9 + bob, _c("skin_shade"))
|
||||||
|
pixels.outline(_ink(shirt), tinted_outlines)
|
||||||
|
if left:
|
||||||
|
pixels.image.flip_x()
|
||||||
|
return pixels
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://jejn3txd0eor
|
||||||
@@ -1,11 +1,46 @@
|
|||||||
class_name WarmStyle
|
class_name WarmStyle
|
||||||
extends ArtStyle
|
extends PixelStyle
|
||||||
## Warm, full colours and dark outlines, close to the classic farming games.
|
## Warm, full colours and dark outlines around figures and things, close to
|
||||||
## Until it draws pixel art, it draws the placeholders of ArtStyle.
|
## the classic farming games.
|
||||||
|
|
||||||
const KEY := &"warm"
|
const KEY := &"warm"
|
||||||
|
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
palette = {
|
||||||
|
"outline": Color("3a2820"), "dust": Color("e9dcc6"),
|
||||||
|
"ground": Color("b98a5e"), "ground_dark": Color("a07650"), "ground_light": Color("cba077"),
|
||||||
|
"pebble": Color("8e8072"), "pebble_light": Color("b8ab9a"), "pebble_dark": Color("6b5f54"),
|
||||||
|
"dead_crack": Color("8a6646"),
|
||||||
|
"soft_top": Color("bcb0a0"), "soft_top_dark": Color("a69a8a"), "soft_top_light": Color("d6ccbd"),
|
||||||
|
"soft_face": Color("8f8170"), "soft_face_dark": Color("776a5b"), "soft_lip": Color("e4dccf"),
|
||||||
|
"hard_top": Color("7c88a2"), "hard_top_dark": Color("68738c"), "hard_top_light": Color("9aa6c0"),
|
||||||
|
"hard_face": Color("596380"), "hard_face_dark": Color("454d66"), "hard_lip": Color("b6c2da"),
|
||||||
|
"hard_glint": Color("dff4ff"),
|
||||||
|
"bed_top": Color("51453e"), "bed_top_dark": Color("433832"), "bed_top_light": Color("62554c"),
|
||||||
|
"bed_face": Color("382e29"), "bed_face_dark": Color("2b231f"), "bed_lip": Color("6e6157"),
|
||||||
|
"grass": Color("64a83f"), "grass_dark": Color("4e8f33"), "grass_light": Color("8acb52"),
|
||||||
|
"grass_edge": Color("3f7a2c"),
|
||||||
|
"flower_a": Color("f7e25c"), "flower_b": Color("fbf6ea"), "flower_c": Color("ef7fa6"),
|
||||||
|
"moss": Color("4e9a3a"), "moss_dark": Color("3b7b2e"), "moss_light": Color("7ec552"), "spore": Color("ffe57e"),
|
||||||
|
"vine": Color("4f9439"), "vine_dark": Color("38702a"), "leaf": Color("68b545"), "leaf_dark": Color("4c8d35"),
|
||||||
|
"stem": Color("5c9c3c"), "bud": Color("acd97a"), "fibre": Color("f8f3e6"), "fibre_shade": Color("dcd3c0"),
|
||||||
|
"seed": Color("7b5433"),
|
||||||
|
"stone": Color("a29a8f"), "stone_light": Color("c8c1b6"), "stone_dark": Color("7c756d"),
|
||||||
|
"wool": Color("f5f0e6"), "wool_shade": Color("dbd2c2"), "wool_light": Color("ffffff"),
|
||||||
|
"face": Color("4b3a31"), "hoof": Color("35281f"),
|
||||||
|
"ruin": Color("aaa395"), "ruin_dark": Color("8a8376"), "ruin_light": Color("c6bfb2"),
|
||||||
|
"ruin_moss": Color("5f9f43"), "door": Color("2b231f"), "glow": Color("ffd560"), "glow_light": Color("fff4c6"),
|
||||||
|
"skin": Color("f4cba5"), "skin_shade": Color("dba67f"), "hair": Color("6e4128"), "hair_light": Color("8f5a37"),
|
||||||
|
"eye": Color("2a1b15"), "pants": Color("4b5b8d"), "pants_dark": Color("3a4672"), "boots": Color("5b3a26"),
|
||||||
|
"marker_near": Color("fff6c2"), "marker_far": Color("ff705c"),
|
||||||
|
}
|
||||||
|
players = [
|
||||||
|
Color("d9573f"), Color("3f7fd1"), Color("e9b23c"), Color("4ea84a"),
|
||||||
|
Color("9a62c9"), Color("39a9a0"), Color("e271a1"), Color("e8873b"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
func key() -> StringName:
|
func key() -> StringName:
|
||||||
return KEY
|
return KEY
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
extends TestCase
|
||||||
|
## The pixel art of the ground, the rock and the figures, which every pixel
|
||||||
|
## style draws.
|
||||||
|
|
||||||
|
|
||||||
|
## How many pixels of `image` are not transparent.
|
||||||
|
static func _opaque(image: Image) -> int:
|
||||||
|
var count := 0
|
||||||
|
for y in image.get_height():
|
||||||
|
for x in image.get_width():
|
||||||
|
if image.get_pixel(x, y).a > 0.0:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
func test_the_field_is_drawn_in_sharp_pixels() -> void:
|
||||||
|
check_eq(ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_filter"), 0, "each pixel a square, not blurred")
|
||||||
|
check(ProjectSettings.get_setting("rendering/2d/snap/snap_2d_transforms_to_pixel"), "on whole pixels of the screen")
|
||||||
|
|
||||||
|
|
||||||
|
func test_ground_fills_its_tile_and_lies_in_the_shadow_of_rock() -> void:
|
||||||
|
var style := WarmStyle.new()
|
||||||
|
var open := style._earth(3, 0).image
|
||||||
|
check_eq(open.get_size(), Vector2i(PixelStyle.TILE, PixelStyle.TILE), "a tile")
|
||||||
|
check_eq(_opaque(open), PixelStyle.TILE * PixelStyle.TILE, "no gaps")
|
||||||
|
var shaded := style._earth(3, TileShape.N).image
|
||||||
|
check(shaded.get_pixel(5, 0).get_luminance() < open.get_pixel(5, 0).get_luminance(), "darker below rock")
|
||||||
|
check_eq(shaded.get_pixel(5, 8), open.get_pixel(5, 8), "but only along the rock")
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_lone_rock_is_round_and_shows_its_face() -> void:
|
||||||
|
var style := WarmStyle.new()
|
||||||
|
var lone := style._rock_image("soft", 0, 0).image
|
||||||
|
check_eq(lone.get_pixel(0, 0).a, 0.0, "a round corner, the ground shows there")
|
||||||
|
check_eq(lone.get_pixel(PixelStyle.TILE - 1, PixelStyle.TILE - 1).a, 0.0, "round at the bottom too")
|
||||||
|
check_eq(lone.get_pixel(8, 0), style.palette["outline"], "outlined at the top")
|
||||||
|
check_eq(lone.get_pixel(8, PixelStyle.TILE - 1), style.palette["outline"], "and where it meets the ground")
|
||||||
|
check_eq(lone.get_pixel(5, PixelStyle.TILE - 3), style.palette["soft_face"], "its face toward the open ground")
|
||||||
|
|
||||||
|
|
||||||
|
func test_rock_inside_a_mass_has_neither_edge_nor_face() -> void:
|
||||||
|
var style := WarmStyle.new()
|
||||||
|
var inside := style._rock_image("soft", 255, 0).image
|
||||||
|
check_eq(_opaque(inside), PixelStyle.TILE * PixelStyle.TILE, "no gaps")
|
||||||
|
for y in PixelStyle.TILE:
|
||||||
|
for x in PixelStyle.TILE:
|
||||||
|
var colour := inside.get_pixel(x, y)
|
||||||
|
check(colour != style.palette["outline"] and colour != style.palette["soft_face"], "inside at %d, %d" % [x, y])
|
||||||
|
|
||||||
|
|
||||||
|
func test_only_the_corners_that_matter_tell_tiles_apart() -> void:
|
||||||
|
check_eq(PixelStyle._shape(TileShape.NE), 0, "a corner alone does not count")
|
||||||
|
check_eq(PixelStyle._shape(TileShape.N | TileShape.E | TileShape.NE), TileShape.N | TileShape.E | TileShape.NE, "between two sides it does")
|
||||||
|
check_eq(PixelStyle._shape(255), 255, "all of them inside a mass")
|
||||||
|
|
||||||
|
|
||||||
|
func test_each_picture_is_made_only_once() -> void:
|
||||||
|
var style := WarmStyle.new()
|
||||||
|
var made := [0]
|
||||||
|
var make := func() -> PixelStyle.Pixels:
|
||||||
|
made[0] += 1
|
||||||
|
return PixelStyle.Pixels.new(1, 1)
|
||||||
|
var first := style._image("test/once", make)
|
||||||
|
var again := style._image("test/once", make)
|
||||||
|
check_eq(again, first, "the same picture")
|
||||||
|
check_eq(made[0], 1, "made once")
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_figure_walks_in_four_steps_and_faces_both_ways() -> void:
|
||||||
|
var style := WarmStyle.new()
|
||||||
|
var frames: Array[PackedByteArray] = []
|
||||||
|
for frame in 5:
|
||||||
|
var image := style._figure_image(0, frame, false).image
|
||||||
|
check_eq(image.get_size(), PixelStyle.FIGURE_SIZE, "a tile wide, one and a half tall")
|
||||||
|
check(not frames.has(image.get_data()), "frame %d differs from the others" % frame)
|
||||||
|
frames.append(image.get_data())
|
||||||
|
var left := style._figure_image(0, 1, true).image
|
||||||
|
left.flip_x()
|
||||||
|
check_eq(left.get_data(), frames[1], "facing left mirrors facing right")
|
||||||
|
var other := style._figure_image(1, 0, false).image
|
||||||
|
check(other.get_data() != frames[0], "another player has another colour")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dr01kkrpn22sy
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
extends TestCase
|
||||||
|
|
||||||
|
|
||||||
|
func test_four_players_have_four_colours() -> void:
|
||||||
|
var style := WarmStyle.new()
|
||||||
|
var colours := {}
|
||||||
|
for player in 4:
|
||||||
|
colours[style.player_color(player)] = true
|
||||||
|
check_eq(colours.size(), 4, "four players, four colours")
|
||||||
|
check_eq(style.player_color(-1), style.player_color(0), "a player not in the world yet looks like the first")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d00aodstjeia5
|
||||||
Reference in New Issue
Block a user