DEMI-PROJECT-PACKAGE Ò{"files":[{"hash":"sha256:2568b244a2f217a5446c8bbf225130ddd790d7b306ee3d82558aeb7edbc2510b","path":"README.md","size":2870},{"hash":"sha256:d5caaadbb21ef9e6f9316d0dca002d659539a12190a5d3b379463d3d5cea5436","path":"scripts/demi/gameplay/character_controller_2d.lua","size":1890},{"hash":"sha256:1e24e4d15f2ba477e874df41c566f5fa29cf29c053521021ee4128b81a91fdd7","path":"scripts/demi/gameplay/controllers.lua","size":1121},{"hash":"sha256:c885379c3f93e3ed81a4288df8b6926e9d10bb39002a0f3747c668b116c2115e","path":"tests/controllers_test.lua","size":865}],"format_version":1,"manifest":{"dependencies":{},"engine":"^0.1.0","exported_events":[],"files":["README.md","scripts/demi/gameplay/controllers.lua","scripts/demi/gameplay/character_controller_2d.lua","tests/controllers_test.lua"],"format_version":1,"name":"demi.gameplay.controllers","public_modules":["demi.gameplay.controllers","demi.gameplay.character_controller_2d"],"version":"1.1.0"},"package_type":"DemiProjectPackage"}# 2D movement controllers Package: `demi.gameplay.controllers`. Provides input-to-intent functions and a separate 2D character helper that calls engine physics and sprite services. Exports `demi.gameplay.controllers` and `demi.gameplay.character_controller_2d`, with no package dependencies or event bus. See [package installation](../../README.md). The intent module works on plain tables: ```lua local Controllers = require("demi.gameplay.controllers") local state = { grounded = true } local config = { coyote_time = 0.1, jump_buffer = 0.1 } local intent intent, state = Controllers.platform({ x = 1, jump = true, dt = 1 / 60 }, state, config) assert(intent.jump) local movement = Controllers.top_down({ x = 1, y = 1 }) assert(movement.x < 1) ``` Keep the returned platform state between calls, refresh `state.grounded` from your collision check, and supply `dt` in seconds. Both timing values must be positive for a jump to trigger; omitted values default to zero. `input.jump` should represent a press, for example `Input.pressed("jump")`. The module does not read input or apply movement itself. `top_down` clamps vectors longer than one and carries optional `aim_x/aim_y`. `click_to_move` returns `{ target_x, target_y }` only when `clicked` is true; it does not find or follow a path. `isometric` returns `grid_x = right - left` and `grid_y = down - up`, without normalization or world projection. For direct platform movement, an engine script can use the character helper. This requires an existing `player` with Transform2D, Rigidbody2D, a collider, and sprite animation clips named `idle`/`run`, plus `move`/`jump` input actions and a `platform` collision layer: ```lua local Character = require("demi.gameplay.character_controller_2d") local controller = Character.new({ move_speed = 5, jump_speed = 9 }) local Player = {} function Player:on_update(dt) controller:update_horizontal("player") controller:try_jump("player", controller:is_grounded("player")) end return Player ``` `is_grounded` casts downward from the entity position, ignoring that entity; adjust `ground_distance` (default 0.56) and `ground_layer` for your collider. `update_horizontal` reads `Input.value`, sets horizontal velocity, flips the sprite when enabled, and selects idle/run animation. `try_jump` reads `Input.pressed` and sets vertical velocity only when passed a grounded state. Action names, speeds, and animation names are constructor options. The character helper does not use the intent module's coyote/buffer state. Neither module configures input bindings, creates entities, supplies navigation, or implements a 3D controller. The game owns state and update timing; there are no events to flush or subscriptions to clean up. From the repository root, test with: ```sh ./build/linux-debug/demi package test packages/sources/demi.gameplay.controllers ``` local CharacterController2D = {} CharacterController2D.__index = CharacterController2D function CharacterController2D.new(options) options = options or {} return setmetatable({ move_action = options.move_action or "move", jump_action = options.jump_action or "jump", ground_layer = options.ground_layer or "platform", move_speed = options.move_speed or 5.0, jump_speed = options.jump_speed or 9.0, ground_distance = options.ground_distance or 0.56, flip_sprite = options.flip_sprite ~= false, idle_animation = options.idle_animation or "idle", run_animation = options.run_animation or "run", }, CharacterController2D) end function CharacterController2D:is_grounded(entity_id) local Transform = require("demi.transform2d") local Physics2D = require("demi.physics.query2d") local x, y = Transform.get_position(entity_id) if x == nil or y == nil then return false end return Physics2D.raycast(x, y, 0.0, -1.0, self.ground_distance, self.ground_layer, entity_id) ~= nil end function CharacterController2D:update_horizontal(entity_id) local Input = require("demi.input") local Rigidbody2D = require("demi.physics.rigidbody2d") local Sprite2D = require("demi.sprite2d") local axis = Input.value(self.move_action) Rigidbody2D.set_velocity_x(entity_id, axis * self.move_speed) if self.flip_sprite and axis ~= 0.0 then Sprite2D.set_flip(entity_id, axis < 0.0, false) end Sprite2D.play_animation(entity_id, axis == 0.0 and self.idle_animation or self.run_animation) return axis end function CharacterController2D:try_jump(entity_id, grounded) local Input = require("demi.input") local Rigidbody2D = require("demi.physics.rigidbody2d") if grounded and Input.pressed(self.jump_action) then Rigidbody2D.set_velocity_y(entity_id, self.jump_speed) return true end return false end return CharacterController2D local Controllers = {} function Controllers.platform(input, state, config) local intent = { x = input.x or 0, jump = false } local grace = state.grounded and (config.coyote_time or 0) or math.max(0, (state.coyote or 0) - input.dt) if state.grounded then grace = config.coyote_time or 0 end local buffer = input.jump and (config.jump_buffer or 0) or math.max(0, (state.jump_buffer or 0) - input.dt) if buffer > 0 and grace > 0 then intent.jump, buffer, grace = true, 0, 0 end return intent, { coyote = grace, jump_buffer = buffer, grounded = state.grounded } end function Controllers.top_down(input) local x, y = input.x or 0, input.y or 0; local length = math.sqrt(x * x + y * y) if length > 1 then x, y = x / length, y / length end return { x = x, y = y, aim_x = input.aim_x, aim_y = input.aim_y } end function Controllers.click_to_move(input) return input.clicked and { target_x = input.x, target_y = input.y } or nil end function Controllers.isometric(input) return { grid_x = (input.right or 0) - (input.left or 0), grid_y = (input.down or 0) - (input.up or 0) } end return Controllers local Test = require("demi.test") local Controllers = require("demi.gameplay.controllers") Test.case("platform coyote and buffered jump are package policy", function() local intent = Controllers.platform({ x = 1, jump = true, dt = 0.01 }, { grounded = false, coyote = 0.05, jump_buffer = 0 }, { coyote_time = 0.1, jump_buffer = 0.1 }) Test.equal(intent.jump, true) end) Test.case("top down diagonal is normalized", function() local intent = Controllers.top_down({ x = 1, y = 1 }) Test.near(math.sqrt(intent.x * intent.x + intent.y * intent.y), 1) end) Test.case("character controller module is independently loadable", function() local Controller = require("demi.gameplay.character_controller_2d") local controller = Controller.new({ move_speed = 7 }) Test.equal(controller.move_speed, 7); Test.equal(controller.ground_layer, "platform") end)