# Component construction Structured components share one construction model: - `Component(...)` maps positional arguments to declared fields. - `Component.new({...})` provides the named form. - `defaults` fill omitted positional values. - `init` validates or derives values after field mapping. - A custom configuration `__call` replaces positional mapping. Storage changes the allocated value, not these rules. ## Positional fields Table components list field names. FFI components list name and C type pairs: ```teal local record Health is tecs.ecs.Component current: integer maximum: integer metamethod __call: function( self, current?: integer, maximum?: integer ): Health end tecs.ecs.newFFIComponent({ name = "Health", container = Health, fields = { {"current", "int32_t"}, {"maximum", "int32_t"}, }, defaults = {100, 100}, }) local full = Health() local damaged = Health(80, 120) ``` Defaults line up with fields. A nil slot means no declared default, while false remains a valid default. FFI allocation supplies zero values for fields that positional arguments and defaults omit. Relationship targets always occupy the first positional argument and do not appear in the public field list. ## Named construction {#table-construction} The named form routes fields through the positional constructor: ```teal local damaged = Health.new({ current = 80, maximum = 120, }) ``` Tecs reads declared fields in order and calls `Health(80, 120)`. The initializer receives positional values, not the input table. Provide a custom `new` only when named construction cannot map to the positional form. ## Validation and derived values `init(instance, ...)` runs after allocation, field assignment, and defaults: ```teal tecs.ecs.newFFIComponent({ name = "Health", container = Health, fields = { {"current", "int32_t"}, {"maximum", "int32_t"}, }, defaults = {100, 100}, init = function(instance: Health) if instance.current < 0 then error("Health.current must not be negative") end if instance.maximum < instance.current then error("Health.maximum must cover current health") end end, }) ``` Use defaults for static values. Use `init` for validation, normalization, and derived state. An initializer requires declared fields or a custom `new`, so the named path stays defined. ## Custom call shapes Supply configuration `__call(instance, ...)` when public constructor arguments do not match stored fields: ```teal tecs.ecs.newComponent({ name = "ParticleEmitter", container = ParticleEmitter, requires = {tecs.Transform2D}, __call = function( instance: ParticleEmitter, options: EmitterOptions ) initEmitter(instance, options) end, new = function(data: {string: any}): ParticleEmitter local instance = {} as ParticleEmitter initEmitter(instance, data as EmitterOptions) return instance end, }) ``` Tecs allocates the base value and applies defaults before the custom call. It does not invoke `init` afterwards. Call shared initialization explicitly when both paths need it.