AgentSkillsCN

enforcing-lgf

适用于 inkmon-godot GDScript 代码,强制执行 Logic Game Framework 的约定。涵盖 Actor 生命周期、共享 Action/Condition/Cost 的无状态特性、AbilitySet/AttributeSet 的访问权限、PreEventConfig 的 Intent 返回值、Resolver 以及事件系统模式。适用于编写或修改触及 Actor、AbilitySet、Action、Condition、Cost、Resolver、PreEventConfig,或事件系统的 GDScript 时使用。

SKILL.md
--- frontmatter
name: enforcing-lgf
description: Enforces Logic Game Framework conventions for inkmon-godot GDScript code. Covers Actor lifecycle, shared Action/Condition/Cost statelessness, AbilitySet/AttributeSet access, PreEventConfig Intent returns, Resolvers, and event system patterns. Use when writing or modifying GDScript that touches Actor, AbilitySet, Action, Condition, Cost, Resolver, PreEventConfig, or the event system.

Logic Game Framework Conventions

Contents

When to use

Apply when writing or modifying GDScript that touches the Logic Game Framework: Actor creation, AbilitySet/AttributeSet access, Action/Condition/Cost implementation, PreEventConfig handlers, Resolvers, or the event system.

Reference


Coding Conventions

1. Attribute Access

Direct access, no getter/setter wrappers. Methods with business logic are fine.

gdscript
# DO
var hp := actor.attribute_set.hp
actor.attribute_set.hp -= damage

# DON'T
func get_hp() -> float:
    return attribute_set.hp

# OK - has logic beyond simple access
func is_alive() -> bool:
    return attribute_set.hp > 0

2. Actor Creation & Registration

Two-step: construct then register.

gdscript
var actor := CharacterActor.new(char_class)
instance.add_actor(actor)  # Framework assigns ID, calls _on_id_assigned()
PhaseActor._idNotes
After .new()Empty stringDo NOT generate ID in _init
After add_actor(){instance_id}:{local_id}Auto-generated, triggers _on_id_assigned()

Override _on_id_assigned() when components created in _init need the actor ID:

gdscript
func _on_id_assigned() -> void:
    ability_set.owner_actor_id = get_id()
    attribute_set.actor_id = get_id()

get_owner_gameplay_instance() uses stored _instance_id + GameWorld.get_instance_by_id() to avoid RefCounted circular references.


3. Shared Object Statelessness (CRITICAL)

TypeOwnershipMutable State?
Ability / AbilityComponentPer characterYES
Action / Condition / Cost / TriggerConfigSHARED via static varNO

static var configs run .new() once at class load. All characters share the same Action/Condition/Cost instances by reference.

RULE: execute() / check() / pay() MUST NOT modify self

gdscript
# WRONG: mutable state in shared Action
class BadAction extends Action.BaseAction:
    var _count := 0
    func execute(ctx: ExecutionContext) -> void:
        _count += 1  # FORBIDDEN - pollutes other characters

# CORRECT: state in external storage
class GoodAction extends Action.BaseAction:
    func execute(ctx: ExecutionContext) -> void:
        var ability_set := _get_owner_ability_set(ctx)
        var count: int = ability_set.tag_container.get_stacks("my_counter")
        ability_set.tag_container.apply_tag("my_counter", -1.0, count + 1)

State Storage:

ScopeLocation
Cross-abilityAbilitySet.tag_container
Single-ability cross-castAbilitySet.tag_container (Tag + Stacks)
Single-castLocal variables in execute()

Debug: logic_game_framework/debug/action_state_check = true in Project Settings.


4. GameStateProvider

IGameStateProvider.get_game_state() intentionally returns Variant — the ONLY acceptable Variant return in the framework.


5. Resolvers

Type-safe delayed evaluation for shared objects. Create via Resolvers factory, evaluate via resolve(ctx).

ResolverFixedDynamic
FloatResolverResolvers.float_val(v)Resolvers.float_fn(fn)
IntResolverResolvers.int_val(v)Resolvers.int_fn(fn)
StringResolverResolvers.str_val(v)Resolvers.str_fn(fn)
DictResolverResolvers.dict_val(v)Resolvers.dict_fn(fn)
Vector3ResolverResolvers.vec3_val(v)Resolvers.vec3_fn(fn)

ParamResolver.resolve_param(resolver: Variant, ctx) accepting Variant is intentional. Prefer typed Resolvers in new code.


6. PreEventConfig Handlers

Signature: func(MutableEvent, AbilityLifecycleContext) -> Intent

Every code path MUST return an Intent. Missing return = null = runtime assertion failure.

IntentFactoryUse Case
Pass throughEventPhase.pass_intent()Condition not met
ModifyEventPhase.modify_intent(id, [Modification])Damage reduction
CancelEventPhase.cancel_intent(id, reason)Immunity, block
gdscript
# Correct: all branches return Intent
func(mutable: MutableEvent, ctx: AbilityLifecycleContext) -> Intent:
    if some_condition:
        return EventPhase.cancel_intent(ctx.ability.id, "immune")
    return EventPhase.pass_intent()

# WRONG: forgot return
func(mutable: MutableEvent, ctx: AbilityLifecycleContext) -> Intent:
    EventPhase.modify_intent(ctx.ability.id, [...])
    # Missing return!

Optional filter: func(Dictionary, AbilityLifecycleContext) -> bool — return true to process event.


7. GameWorld Dependency

Framework directly references GameWorld Autoload. This is intentional — do not attempt to decouple.


Standard Workflow

When implementing new game logic that touches the framework, follow these steps:

  1. Identify scope → Is this an Actor, Ability, Action, PreEvent, or System?
    • New Actor: Follow §2 (construct → register → _on_id_assigned)
    • New Ability: Use AbilityConfig.builder(), see reference/abilities.md
    • New Action: Extend Action.BaseAction, ensure statelessness (§3)
    • New PreEvent handler: Follow §6 (every path returns Intent)
  2. Check shared vs owned → Refer to §3 ownership table. If shared (static var), MUST NOT store mutable state in self.
  3. Use Resolvers for dynamic params → If an Action needs runtime values, use Resolvers factory (§5) instead of storing state.
  4. Implement → Write the code following conventions above.
  5. Validate → Run the checklist below.

Validation Checklist

Before considering implementation complete, verify:

  • Actor IDs: NOT generated in _init; _on_id_assigned() syncs ID to components
  • Shared objects (Action/Condition/Cost/TriggerConfig): execute()/check()/pay() do NOT modify self
  • State storage: cross-ability → tag_container; single-cast → local variables
  • Attribute access: direct actor.attribute_set.x, no trivial getter/setter wrappers
  • PreEventConfig handlers: EVERY code path returns an Intent (pass/modify/cancel)
  • Resolvers: dynamic values use Resolvers.float_fn() etc., not instance fields
  • No attempts to decouple GameWorld Autoload dependency
  • IGameStateProvider.get_game_state() returning Variant is intentional — do not "fix"