> ## Documentation Index
> Fetch the complete documentation index at: https://docs.summerengine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Painted_player (Painted 25d world skill)

> painted_player.gd from the Painted 25d world skill in the Summer Engine skill library. Build a 2D or 2.5D game as a painted world: a 3D blockout under one fixed camera, an AI paint-over projected onto it, sprite characters.

> [!NOTE]
>
> Part of the [`painted-25d-world` skill](/skills/painted-25d-world). The skill links here when your agent needs the detail.

```gdscript painted_player.gd theme={null}
extends CharacterBody3D
## Sprite character on a capsule. Input moves in screen directions: up = away from the camera on the ground.
## The sprite is a vertical Y-billboard, stretched by 1 / cos(camera pitch) so it reads at full height from the
## tilted camera. Eight facings picked by 45 degree sectors; a missing diagonal falls back to its side row.
## The gait depends on the game type. Cozy, exploration and RPG games walk by default (about 1.8 m/s, walk cycle)
## and run on Shift (about 4 m/s, run cycle); action games may run by default (move_speed about 4.2). The cycle is
## chosen by speed (run cycle from `run_cycle_from`), and speed_scale = speed / the cycle's natural speed, so the
## feet never slide. Never play the run cycle for the default gait of a cozy game.
## Sheet: one row per animation (idle_down, walk_up_left, run_right, ...), fixed cell size. The json beside it:
## {"cell_w", "cell_h", "frames", "rows", "fps": {"idle": 6, "walk": 10, "run": 14}, "height_m": 1.8,
##  "stride_m": {"walk": 0.94, "run": 1.68}, "cycles_per_s": {"walk": 1.25, "run": 1.75}}
## (stride_m = metres per cycle at height_m).

const DIRS := ["right", "up_right", "up", "up_left", "left", "down_left", "down", "down_right"]

@export var sheet: Texture2D
@export_file("*.json") var sheet_json := ""
## About door height / 1.15: 2.15 m next to 2.5 m doors.
@export var height_m := 2.15
## m/s, the default gait. Cozy / exploration / RPG: 1.8 (walk cycle). Action: about 4.2 (run cycle).
@export var move_speed := 1.8
## m/s while Shift is held (run cycle). Cozy: 4.0. Action: up to 1.8x the run cycle's natural speed.
@export var sprint_speed := 4.0
## m/s from which the run cycle plays instead of the walk cycle.
@export var run_cycle_from := 3.0
## Tests and agents drive the character with this instead of keys (x = screen right, y = screen up):
## length up to 1 is the default gait, above 1 sprints.
var input_override := Vector2.ZERO
var cam_basis := Basis()
var sprite: AnimatedSprite3D
var facing := "down"
var meta: Dictionary

func _ready() -> void:
	meta = JSON.parse_string(FileAccess.get_file_as_string(sheet_json))
	var shape := CollisionShape3D.new()
	var cap := CapsuleShape3D.new()
	cap.radius = 0.4
	cap.height = height_m - 0.2
	shape.shape = cap
	shape.position.y = cap.height / 2.0
	add_child(shape)
	sprite = AnimatedSprite3D.new()
	sprite.sprite_frames = _frames()
	sprite.billboard = BaseMaterial3D.BILLBOARD_FIXED_Y
	sprite.shaded = false
	sprite.alpha_cut = SpriteBase3D.ALPHA_CUT_DISCARD   # writes depth, so painted walls hide the character
	add_child(sprite)

## Called by the world with the paint camera's basis.
func setup(view_basis: Basis) -> void:
	cam_basis = view_basis
	var pitch := asin(clampf(-view_basis.z.y, -1.0, 1.0))
	var h := height_m / cos(pitch)
	sprite.pixel_size = h / float(meta.cell_h)
	sprite.position.y = h / 2.0
	_play("idle", 0.0)

func _frames() -> SpriteFrames:
	var sf := SpriteFrames.new()
	sf.remove_animation("default")
	for r in meta.rows.size():
		var anim: String = meta.rows[r]
		sf.add_animation(anim)
		sf.set_animation_speed(anim, _fps(anim.get_slice("_", 0)))
		for c in int(meta.frames):
			var at := AtlasTexture.new()
			at.atlas = sheet
			at.region = Rect2(c * meta.cell_w, r * meta.cell_h, meta.cell_w, meta.cell_h)
			sf.add_frame(anim, at)
	return sf

## fps in the json: one number, or a dictionary by kind ("walk").
func _fps(kind: String) -> float:
	var fps = meta.get("fps", 8.0)
	if fps is Dictionary:
		return float(fps.get(kind, 8.0))
	return float(fps)

## Ground speed (m/s) at which this kind's cycle keeps the feet still: stride (scaled to this height) x cycles/s.
## Without measurements: the generated cycles' typical values (walk 0.57, run 1.63 body heights per second).
func natural_speed(kind: String) -> float:
	var stride = meta.get("stride_m", {}).get(kind)
	if stride == null:
		return height_m * (1.63 if kind == "run" else 0.57)
	var cycles = meta.get("cycles_per_s", {}).get(kind, _fps(kind) / float(meta.frames))
	return float(stride) * height_m / float(meta.get("height_m", height_m)) * float(cycles)

## The cycle for a ground speed: the walk cycle below `run_cycle_from`, the run cycle from there.
func cycle_for(speed: float) -> String:
	if not sprite.sprite_frames.has_animation("run_down") or speed < run_cycle_from:
		return "walk"
	return "run"

func _play(kind: String, speed: float) -> void:
	var side := "left" if facing.ends_with("left") else ("right" if facing.ends_with("right") else facing)
	for anim in [kind + "_" + facing, kind + "_" + side, kind + "_down"]:
		if sprite.sprite_frames.has_animation(anim):
			sprite.play(anim)
			break
	sprite.speed_scale = 1.0 if kind == "idle" else clampf(speed / natural_speed(kind), 0.25, 4.0)

func _physics_process(delta: float) -> void:
	var v := input_override
	var speed := move_speed
	if v != Vector2.ZERO:
		speed = sprint_speed if v.length() > 1.05 else move_speed
	else:
		v = Input.get_vector("ui_left", "ui_right", "ui_down", "ui_up")
		if Input.is_key_pressed(KEY_SHIFT):
			speed = sprint_speed
	if v.length() < 0.1:
		speed = 0.0
	var right := Vector3(cam_basis.x.x, 0, cam_basis.x.z).normalized()
	var far := Vector3(-cam_basis.z.x, 0, -cam_basis.z.z).normalized()
	var dir := (right * v.x + far * v.y).normalized()
	velocity.x = dir.x * speed
	velocity.z = dir.z * speed
	velocity.y = 0.0 if is_on_floor() else velocity.y - 20.0 * delta
	move_and_slide()
	if speed > 0.0:
		facing = DIRS[wrapi(roundi(v.angle() / (PI / 4.0)), 0, 8)]
		_play(cycle_for(speed), speed)
	else:
		_play("idle", 0.0)
```

***

Source: [`library/skills/painted-25d-world/_building-blocks/painted_player.gd`](https://github.com/SummerEngine/summer/blob/main/library/skills/painted-25d-world/_building-blocks/painted_player.gd), rendered from `summer-engine@4.5.0` on npm.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.