Files
sporel-module-spine-prototype/init.lua
Axel Meyer 0227aad47d feat(0.3.0): Player als Actor-Entity (Phase A.6)
Refactor spine-prototype für player_control v0.2.0 + actor:
- composition.define_template{id="player", properties={position,
  movement_speed, sprite_color, sprite_w, sprite_h}, tags={"renderable"}}
- actor.create instanziiert Player bei map-center
- player_control.update(player, dt) ersetzt player.update(dt)
- camera.set_target / interaction.update lesen via actor.position(player)
  (Lua-multi-return)
- Manueller draw_rect für Player raus — Player wird via
  render.draw_entities{tag="renderable"} zusammen mit Sign gezeichnet
- Draw-order: stones zuerst, dann Entities (Sign + Player). Stones-vs-
  Player-Overlay-Issue in der Praxis nicht relevant (Positionen kollidieren
  nicht)

PLAYER_R/G/B Konstanten entfernt (Color jetzt im Template).

Bumps: 0.2.0 → 0.3.0; +actor v0.1.0; player_control 0.1.0 → 0.2.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-09 14:13:17 +00:00

234 lines
8.7 KiB
Lua

local maps = require("lib-core.maps")
local r_lib = require("lib-core.render")
local camera = require("lib-core.camera")
local input = require("lib-core.input")
local player_control = require("lib-core.player_control")
local interaction = require("lib-core.interaction")
local selection = require("lib-core.selection")
local command = require("lib-core.command")
local composition = require("lib-core.composition")
local actor = require("lib-core.actor")
local demo_map = maps.load("maps/demo.map.json")
maps.set_current(demo_map)
maps.load_textures(engine.module.asset_aliases())
local m_size = maps.size()
local t_size = maps.tile_size()
local map_center_x = (m_size.w * t_size) / 2
local map_center_y = (m_size.h * t_size) / 2
camera.set_target(map_center_x, map_center_y)
camera.set_zoom(1.0)
-- Bind input actions (string-key-names per lib-core.input v0.2.0)
input.bind("quit", { "escape" })
input.bind("move_left", { "a", "left" })
input.bind("move_right", { "d", "right" })
input.bind("move_up", { "w", "up" })
input.bind("move_down", { "s", "down" })
input.bind("interact", { "e" })
-- P.3.5: Free-Cam demo (Tab toggle)
input.bind("toggle_free_cam", { "tab" })
input.bind("pan_drag", { "mouse_middle" })
-- pan_*-Actions teilen sich die Keys mit move_*-Actions (additive bindings).
input.bind("pan_left", { "a", "left" })
input.bind("pan_right", { "d", "right" })
input.bind("pan_up", { "w", "up" })
input.bind("pan_down", { "s", "down" })
-- P.3.6: Selection bindings
input.bind("lmb", { "mouse_left" })
input.bind("mod_add", { "shift" })
input.bind("mod_toggle", { "ctrl" })
selection.bind_action("lmb")
selection.bind_modifier_add("mod_add")
selection.bind_modifier_toggle("mod_toggle")
-- P.3.7: Command bindings (RMB → move selected units).
input.bind("rmb", { "mouse_right" })
command.bind_move_action("rmb")
command.bind_target_provider(function() return selection.list() end)
camera.bind_pan_keys("pan_left", "pan_right", "pan_up", "pan_down")
camera.bind_pan_drag("pan_drag")
camera.enable_pan_edge()
-- Start in follow-mode: pan_keys aus (drag/edge bleiben enabled, sind aber
-- unsichtbar in follow-mode wegen set_target-override jeden Frame).
camera.set_pan_enabled("keys", false)
-- Player-Template (Phase A.5/A.6 — Actor + composition).
-- position is the visual CENTER per render.draw_entities convention.
composition.define_template{
id = "player",
properties = {
position = {x = 0, y = 0},
movement_speed = 200,
sprite_color = engine.render.rgb(200, 60, 80),
sprite_w = 24,
sprite_h = 24,
},
tags = {"renderable"},
}
-- Spawn the player at map-center; bind WASD movement.
local player = actor.create{
template = "player",
properties = {
position = {x = map_center_x, y = map_center_y},
movement_speed = 200,
},
}
player_control.bind_movement("move_left", "move_right", "move_up", "move_down")
-- Sign-Template (Phase A.1 — composition + Inactive-Entity-Pattern).
-- Position-Property is the visual CENTER per render.draw_entities convention.
composition.define_template{
id = "sign",
properties = {
sprite_color = engine.render.rgb(220, 200, 60),
sprite_w = 12,
sprite_h = 12,
text = "default sign text",
position = {x = 0, y = 0},
},
tags = {"renderable"},
}
-- Instantiate the demo sign at tile (4,4)-center = 144 px in grass-area.
local sign = composition.create{
template = "sign",
properties = {
position = {x = 144, y = 144},
text = "Hier steht: Willkommen im Spine.",
},
}
-- P.3.6/P.3.7: 6 stones as selectables + movable units (3 speed classes).
local stones = {
{ x = 64, y = 96, w = 20, h = 20, color = {120, 120, 140}, speed = 100 },
{ x = 224, y = 96, w = 20, h = 20, color = {120, 120, 140}, speed = 100 },
{ x = 96, y = 256, w = 20, h = 20, color = {130, 110, 100}, speed = 150 },
{ x = 320, y = 192, w = 20, h = 20, color = {130, 110, 100}, speed = 150 },
{ x = 192, y = 320, w = 20, h = 20, color = {140, 130, 110}, speed = 200 },
{ x = 384, y = 256, w = 20, h = 20, color = {140, 130, 110}, speed = 200 },
}
-- P.3.5: Free-Cam-Mode state
local free_cam_mode = false
local frames = 0
local TARGET_FRAMES = 60
function init(ctx)
engine.print("spine-prototype init: maps loaded")
engine.print(string.format("map size: %dx%d", m_size.w, m_size.h))
local corner = maps.tile_at(0, 0)
local interior = maps.tile_at(5, 5)
engine.print("tile at (0,0): " .. corner.id)
engine.print("tile at (5,5): " .. interior.id)
engine.print("walkable at (0,0): " .. tostring(maps.is_walkable(0, 0)))
engine.print("walkable at (5,5): " .. tostring(maps.is_walkable(5, 5)))
engine.print(string.format("render: %d tiles per frame", m_size.w * m_size.h))
local t = camera.target()
engine.print(string.format("camera: target=(%d,%d) zoom=%.2f", t.x, t.y, camera.zoom()))
engine.print(string.format("input: %d actions bound", input.action_count()))
local px, py = actor.position(player)
local sw = player:get_property("sprite_w")
local sh = player:get_property("sprite_h")
engine.print(string.format("player: pos=(%d,%d) size=%dx%d speed=%d",
px, py, sw, sh, actor.movement_speed(player)))
-- Position + text are read once at registration; the sign is stationary
-- in v0.1 so a snapshot is correct. If it ever moves, the interaction-
-- API would need closure-form anchor (currently raw-number-only).
interaction.register(
sign:get_property("position.x"),
sign:get_property("position.y"),
40, "interact",
function(info)
engine.print(sign:get_property("text"))
end)
engine.print(string.format("interaction: %d trigger(s) registered",
interaction.trigger_count()))
-- P.3.6/P.3.7: Register each stone as selectable AND movable.
for _, s in ipairs(stones) do
s.sel_handle = selection.register(function()
return { x = s.x, y = s.y, w = s.w, h = s.h }
end)
s.cmd_handle = command.register(
function() return { x = s.x, y = s.y } end,
function(nx, ny) s.x = nx; s.y = ny end,
s.speed
)
end
engine.print(string.format("selection: %d selectables registered",
selection.count_registered()))
engine.print(string.format("command: %d movable units registered",
command.count_registered()))
end
function render(ctx)
engine.render.clear_color(engine.render.rgb(20, 20, 30))
camera.begin()
maps.draw_map_pre_entities()
-- P.3.6: draw stones (still Lua-tables; future-slice makes them
-- entities too)
for _, s in ipairs(stones) do
engine.render.draw_rect(s.x, s.y, s.w, s.h,
engine.render.rgb(s.color[1], s.color[2], s.color[3]))
end
-- Draw all renderable entities (Sign + Player). Drawn AFTER
-- stones so the Player visually overlays stone-occlusions —
-- in practice positions don't collide, but the layering
-- intent is preserved.
r_lib.draw_entities{ tag = "renderable" }
-- P.3.6: highlights over selected entities (world-space)
selection.render_highlights()
maps.draw_map_post_entities()
camera.finish()
-- P.3.6: drag-box overlay (screen-space, after camera.finish)
selection.render_drag_box()
end
function update(ctx, dt)
if input.was_action_pressed("quit") then
-- ADR-0028: ESC returns to launcher per P.3.4.
engine.switch_module("lib-management.launcher")
return
end
-- P.3.5: Tab toggles between follow-cam and free-cam.
if input.was_action_pressed("toggle_free_cam") then
free_cam_mode = not free_cam_mode
camera.set_pan_enabled("keys", free_cam_mode)
end
-- Player + interaction nur in follow-mode (in free-cam ist player pausiert).
if not free_cam_mode then
player_control.update(player, dt)
interaction.update(actor.position(player))
end
-- Pan-Summation jeden Frame (keys nur wenn enabled; drag + edge always-on).
camera.update(dt)
selection.update(dt)
command.update(dt)
if not free_cam_mode then
-- Follow-cam: set_target jeden Frame -> drag/edge "drift" wird unsichtbar.
camera.set_target(actor.position(player))
end
-- CI-conditional self-exit (unverändert aus P.0):
frames = frames + 1
if os.getenv("SPOREL_CI") == "1" and frames >= TARGET_FRAMES then
engine.exit(0)
end
end
function cleanup(ctx)
engine.print("spine-prototype cleanup: total_frames=" .. frames)
end