orbit/js/visuals/SystemEffects.js

627 lines
23 KiB
JavaScript

/**
* Per-system visual effects — the star's "character".
*
* An effect is a BUNDLE of sub-effects, all data-driven off
* data/systems.json → types.<type>.effect (tuning = a JSON edit). Two kinds:
*
* CAMERA FILTERS (WebGL; the HUD is excluded by the UI-camera split):
* - ripple: concentric waves, 1 or 2 centers. One center (nebula,
* redDwarf) radiates from the view middle ("screen") or from the
* star's direction ("star"); two centers can orbit each other
* (orbit.radius/period). Every sub-effect is OPTIONAL — a type wears
* exactly what it lists (binary, for example, has only the grade +
* the wandering star — no ripple at all).
* - grade: a per-pixel color grade — tint + saturation + brightness,
* or a two-color directional split, plus a flare flash (redDwarf).
*
* CONTENT (world-space sprites, Canvas-safe, behind the art at depth 3):
* - particles (redDwarf ember dust) → js/visuals/EmberField.js
* - wanderer (binary companion star) → js/visuals/WandererStar.js
* (Those are created by GameScene, not here — this file owns the
* camera filters.)
*
* Why camera-level (not per-object): a system is a PLACE. Its effect
* should bend the light of everything in it — the starfield included —
* as one continuous distortion. Per-body filters would need one composite
* pass per body (200+ starfield sprites, no) and the stars could never
* warp. One or two camera filters = a couple of full-screen passes,
* constant cost, whole scene coheres. The HUD is excluded by drawing it
* on a separate, unfiltered camera (js/visuals/UiCameras.js).
*
* The "flare" (redDwarf) is a shared signal: one seeded rhythm drives BOTH
* the ripple's amplitude surge AND the grade's brightness flash, so the
* wave and the light flash together — the star flaring.
*
* WebGL only for the filters. On the canvas renderer the filters are a
* no-op (the game degrades to no filter, never to a crash); the CONTENT
* sub-effects still render (they are plain sprites).
*
* Node-testable math lives in SystemEffectsMath.js (this file imports
* Phaser and is exercised in the browser via dev/server.mjs).
*/
import Phaser from '../vendor/phaser.js';
import { config } from '../config/Config.js';
import {
ripplePhase,
worldToUV,
flareIntensity,
orbitCenters,
hexToRgb01,
tintMultiplier,
} from './SystemEffectsMath.js';
import { ensureUiCameras } from './UiCameras.js';
/** Registered render-node names (renderer-global). */
export const RIPPLE_NODE = 'FilterRippleEffect';
export const GRADE_NODE = 'FilterGradeEffect';
/**
* The ripple fragment shader — generalized to up to TWO centers. Each
* center contributes a traveling radial wave (phase moves outward); the
* total displacement is the sum. A center with amplitude 0 is inert, so a
* one-center system (nebula, redDwarf) is the degenerate case. Phaser 4
* filter-shader conventions: `uMainSampler` is the input frame,
* `outTexCoord` the screen UV, `boundedSampler` auto-injected.
*/
const RIPPLE_FRAGMENT = [
'#pragma phaserTemplate(shaderName)',
'precision mediump float;',
'uniform sampler2D uMainSampler;',
'uniform float time;',
'uniform float strength;',
'uniform float amp0;',
'uniform float amp1;',
'uniform float cx0;',
'uniform float cy0;',
'uniform float cx1;',
'uniform float cy1;',
'varying vec2 outTexCoord;',
'#pragma phaserTemplate(fragmentHeader)',
'void main()',
'{',
' vec2 disp = vec2(0.0);',
// center 0
' vec2 d0 = outTexCoord - vec2(cx0, cy0);',
' float r0 = length(d0);',
' if (amp0 > 0.0001 && r0 > 0.0001) {',
' vec2 dir0 = d0 / r0;',
' float w0 = sin(r0 * strength - time);',
' float f0 = smoothstep(0.0, 0.02, r0) * (0.8 + 0.2 * exp(-r0 * 0.25));',
' disp += dir0 * (w0 * amp0 * f0);',
' }',
// center 1
' vec2 d1 = outTexCoord - vec2(cx1, cy1);',
' float r1 = length(d1);',
' if (amp1 > 0.0001 && r1 > 0.0001) {',
' vec2 dir1 = d1 / r1;',
' float w1 = sin(r1 * strength - time);',
' float f1 = smoothstep(0.0, 0.02, r1) * (0.8 + 0.2 * exp(-r1 * 0.25));',
' disp += dir1 * (w1 * amp1 * f1);',
' }',
' gl_FragColor = boundedSampler(uMainSampler, outTexCoord + disp);',
'}',
].join('\n');
/**
* The grade fragment shader — per-pixel color grade (the exact math of
* gradeColor in SystemEffectsMath.js, as a thin GLSL wrapper):
* brightness → saturation → tint → directional split → flare flash.
* All uniforms are scalars (no vec packing) for portability.
*/
const GRADE_FRAGMENT = [
'#pragma phaserTemplate(shaderName)',
'precision mediump float;',
'uniform sampler2D uMainSampler;',
'uniform float uBright;',
'uniform float uSat;',
'uniform float uTintAmt;',
'uniform float uTintR;',
'uniform float uTintG;',
'uniform float uTintB;',
'uniform float uSplitMix;',
'uniform float uSplitAR;',
'uniform float uSplitAG;',
'uniform float uSplitAB;',
'uniform float uSplitBR;',
'uniform float uSplitBG;',
'uniform float uSplitBB;',
'uniform float uAxisX;',
'uniform float uAxisY;',
'uniform float uFlash;',
'uniform float uFlashR;',
'uniform float uFlashG;',
'uniform float uFlashB;',
'uniform float uLiftAmt;',
'uniform float uLiftR;',
'uniform float uLiftG;',
'uniform float uLiftB;',
'uniform float uGrainAmt;',
'uniform float uGrainOffX;',
'uniform float uGrainOffY;',
'varying vec2 outTexCoord;',
'#pragma phaserTemplate(fragmentHeader)',
'void main()',
'{',
' vec4 col = boundedSampler(uMainSampler, outTexCoord);',
' vec3 rgb = col.rgb;',
' rgb *= uBright;',
' if (uSat != 1.0) {',
' float l = dot(rgb, vec3(0.299, 0.587, 0.114));',
' rgb = mix(vec3(l), rgb, uSat);',
' }',
' if (uTintAmt > 0.0) {',
' vec3 tint = vec3(uTintR, uTintG, uTintB);',
' rgb = mix(rgb, rgb * tint, uTintAmt);',
' }',
' // Shadow lift: push the DARKNESS toward the lift hue (the red void),',
' // leaving bright pixels (the stars) alone. Luminance-gated.',
' if (uLiftAmt > 0.0) {',
' float lum2 = dot(rgb, vec3(0.299, 0.587, 0.114));',
' float darkAmt = (1.0 - clamp(lum2 * 3.0, 0.0, 1.0)) * uLiftAmt;',
' rgb += vec3(uLiftR, uLiftG, uLiftB) * darkAmt;',
' }',
' // Gritty grain: fine, warm, only in the dark (a thin red haze). The',
' // coordinate is offset by the camera scroll (uGrainOff) so the grain',
' // sits at a DEPTH in the air — it shifts slightly as you fly, instead',
' // of being stuck to the lens (a subtle parallax).',
' if (uGrainAmt > 0.0) {',
' float lum3 = dot(rgb, vec3(0.299, 0.587, 0.114));',
' float gdark = 1.0 - clamp(lum3 * 3.0, 0.0, 1.0);',
' vec2 gp = gl_FragCoord.xy + vec2(uGrainOffX, uGrainOffY);',
' float gnoise = fract(sin(dot(gp, vec2(12.9898, 78.233))) * 43758.5453);',
' rgb += (gnoise - 0.5) * uGrainAmt * gdark * vec3(1.0, 0.55, 0.45);',
' }',
' if (uSplitMix > 0.0) {',
' float alen = length(vec2(uAxisX, uAxisY));',
' vec2 an = alen > 0.0001 ? vec2(uAxisX, uAxisY) / alen : vec2(1.0, 0.0);',
' float s = dot(outTexCoord - vec2(0.5, 0.5), an) * 2.0;',
' s = clamp(s * 0.5 + 0.5, 0.0, 1.0);',
' vec3 a = vec3(uSplitAR, uSplitAG, uSplitAB);',
' vec3 b = vec3(uSplitBR, uSplitBG, uSplitBB);',
' rgb = mix(rgb, rgb * (b + (a - b) * s), uSplitMix);',
' }',
' if (uFlash > 0.0) rgb += vec3(uFlashR, uFlashG, uFlashB) * uFlash;',
' gl_FragColor = vec4(rgb, col.a);',
'}',
].join('\n');
// ---------------------------------------------------------------------------
// RIPPLE
// ---------------------------------------------------------------------------
/**
* The ripple's state carrier (Phaser.Filters.Controller) — the values the
* shader reads each pass: time (radians), strength (cycles/UV), and the
* two centers' UVs + amplitudes. Plain data.
*
* @extends {Phaser.Filters.Controller}
*/
class RippleController extends Phaser.Filters.Controller {
/** @param {object} camera the world camera */
constructor(camera) {
super(camera, RIPPLE_NODE);
// uniforms
this.time = 0;
this.strength = 18;
this.amp0 = 0.004;
this.amp1 = 0;
this.cx0 = 0.5;
this.cy0 = 0.5;
this.cx1 = 0.5;
this.cy1 = 0.5;
// parameters (set at apply(), read in update())
this.baseAmp = 0.004;
this.speed = 1;
this.anchor = 'screen';
this.centers = 1;
this.orbit = null; // { radius, period } (binary)
this.flare = null; // { interval, duration, rippleBoost } (redDwarf)
this.phase = 0; // per-system 0..1 (flare rhythm / orbit start)
}
}
/**
* The ripple render node (BaseFilterShader) — a full-screen quad through
* which the camera's composited frame flows, displaced per the
* controller's uniforms.
*
* @extends {Phaser.Renderer.WebGL.RenderNodes.BaseFilterShader}
*/
class FilterRippleEffect extends Phaser.Renderer.WebGL.RenderNodes.BaseFilterShader {
/** @param {object} manager the RenderNodes manager */
constructor(manager) {
super(RIPPLE_NODE, manager, null, RIPPLE_FRAGMENT);
}
/** Push this pass's controller values into the shader. */
setupUniforms(controller, _drawingContext) {
const pm = this.programManager;
pm.setUniform('time', controller.time);
pm.setUniform('strength', controller.strength);
pm.setUniform('amp0', controller.amp0);
pm.setUniform('amp1', controller.amp1);
pm.setUniform('cx0', controller.cx0);
pm.setUniform('cy0', controller.cy0);
pm.setUniform('cx1', controller.cx1);
pm.setUniform('cy1', controller.cy1);
}
}
/**
* Register the ripple node (idempotent — the registry is renderer-global
* and throws on a duplicate constructor).
*
* @param {object} renderer Phaser renderer
* @returns {boolean} true if the node is registered after the call
*/
export function ensureRippleNode(renderer) {
return registerNode(renderer, RIPPLE_NODE, FilterRippleEffect);
}
// ---------------------------------------------------------------------------
// GRADE
// ---------------------------------------------------------------------------
/**
* The grade's state carrier — the per-pixel color-grade values the shader
* reads. The static parts (bright/sat/tint/split) are set once at apply();
* the flare flash (uFlash) is advanced each frame from the shared flare.
*
* @extends {Phaser.Filters.Controller}
*/
class GradeController extends Phaser.Filters.Controller {
/** @param {object} camera the world camera */
constructor(camera) {
super(camera, GRADE_NODE);
// uniforms (all scalars)
this.uBright = 1;
this.uSat = 1;
this.uTintAmt = 0;
this.uTintR = 1;
this.uTintG = 1;
this.uTintB = 1;
this.uSplitMix = 0;
this.uSplitAR = 1;
this.uSplitAG = 1;
this.uSplitAB = 1;
this.uSplitBR = 1;
this.uSplitBG = 1;
this.uSplitBB = 1;
this.uAxisX = 1;
this.uAxisY = 0;
this.uFlash = 0;
this.uFlashR = 1;
this.uFlashG = 1;
this.uFlashB = 1;
// shadow lift (the red void) + gritty grain — redDwarf; 0 = off
this.uLiftAmt = 0;
this.uLiftR = 0.75;
this.uLiftG = 0.22;
this.uLiftB = 0.17;
this.uGrainAmt = 0;
this.uGrainOffX = 0;
this.uGrainOffY = 0;
// grain parallax (0 = stuck to the lens; >0 = shifts as the camera moves)
this.grainParallax = 0;
// parameter for update(): the flare's base flash strength (0..1)
this.flareFlash = 0;
}
}
/**
* The grade render node (BaseFilterShader).
*
* @extends {Phaser.Renderer.WebGL.RenderNodes.BaseFilterShader}
*/
class FilterGradeEffect extends Phaser.Renderer.WebGL.RenderNodes.BaseFilterShader {
/** @param {object} manager the RenderNodes manager */
constructor(manager) {
super(GRADE_NODE, manager, null, GRADE_FRAGMENT);
}
/** Push this pass's controller values into the shader. */
setupUniforms(controller, _drawingContext) {
const pm = this.programManager;
const c = controller;
pm.setUniform('uBright', c.uBright);
pm.setUniform('uSat', c.uSat);
pm.setUniform('uTintAmt', c.uTintAmt);
pm.setUniform('uTintR', c.uTintR);
pm.setUniform('uTintG', c.uTintG);
pm.setUniform('uTintB', c.uTintB);
pm.setUniform('uSplitMix', c.uSplitMix);
pm.setUniform('uSplitAR', c.uSplitAR);
pm.setUniform('uSplitAG', c.uSplitAG);
pm.setUniform('uSplitAB', c.uSplitAB);
pm.setUniform('uSplitBR', c.uSplitBR);
pm.setUniform('uSplitBG', c.uSplitBG);
pm.setUniform('uSplitBB', c.uSplitBB);
pm.setUniform('uAxisX', c.uAxisX);
pm.setUniform('uAxisY', c.uAxisY);
pm.setUniform('uFlash', c.uFlash);
pm.setUniform('uFlashR', c.uFlashR);
pm.setUniform('uFlashG', c.uFlashG);
pm.setUniform('uFlashB', c.uFlashB);
pm.setUniform('uLiftAmt', c.uLiftAmt);
pm.setUniform('uLiftR', c.uLiftR);
pm.setUniform('uLiftG', c.uLiftG);
pm.setUniform('uLiftB', c.uLiftB);
pm.setUniform('uGrainAmt', c.uGrainAmt);
pm.setUniform('uGrainOffX', c.uGrainOffX);
pm.setUniform('uGrainOffY', c.uGrainOffY);
}
}
/** Register the grade node (idempotent). */
export function ensureGradeNode(renderer) {
return registerNode(renderer, GRADE_NODE, FilterGradeEffect);
}
// ---------------------------------------------------------------------------
// Facade
// ---------------------------------------------------------------------------
/**
* Register a render-node constructor (shared by ripple + grade).
*
* @param {object} renderer
* @param {string} name
* @param {Function} ctor
* @returns {boolean}
*/
function registerNode(renderer, name, ctor) {
const nodes = renderer?.renderNodes;
if (!nodes || typeof nodes.hasNode !== 'function') return false;
if (nodes.hasNode(name)) return true;
try {
nodes.addNodeConstructor(name, ctor);
return true;
} catch (err) {
console.warn(`[orbit] could not register the ${name} filter node: ${err}`);
return false;
}
}
/** Read a finite number from `src[key]`, else `fallback`. */
function num(src, key, fallback) {
const x = Number(src?.[key]);
return Number.isFinite(x) ? x : fallback;
}
/**
* The facade GameScene drives: apply the system type's effect bundle on
* entry, advance it per frame, release on exit. Holds NO scene-lifetime
* assumptions of its own (the scene's shutdown() calls release()).
*
* Attaches to the WORLD camera (each optional — the type wears what it
* lists): the ripple (when the bundle has a `ripple`) and the grade (when
* it has a `grade`), both in this file. The content sub-effects
* (particles / wanderer) are GameScene's job.
*/
export class SystemEffects {
/** @param {object} scene Phaser scene */
constructor(scene) {
this.scene = scene;
this.controller = null; // ripple
this.grade = null; // grade
this.kind = null;
this.flare = null; // the shared flare params (or null)
this.phase = 0; // per-system 0..1
}
/** True while any filter is attached to the world camera. */
get active() {
return this.controller !== null || this.grade !== null;
}
/**
* Apply the system type's effect bundle (data/systems.json →
* types.<type>.effect). Replaces any current effect. No-op for an empty
* bundle and for non-WebGL renderers (the content sub-effects are
* independent and still render).
*
* @param {string} systemType one of systems.types keys
* @returns {boolean} whether a filter ended up active
*/
apply(systemType) {
this.release();
const eff = config.get(`systems.types.${systemType}.effect`, null);
if (!eff || typeof eff !== 'object') {
this.kind = null;
return false;
}
// Every sub-effect is optional — a type wears exactly what it lists
// (an empty {} bundle means no filter at all).
const ripple = eff.ripple;
const hasRipple = !!(ripple && Number(ripple.amplitude) > 0);
const hasGrade = !!eff.grade;
if (!hasRipple && !hasGrade) {
this.kind = null;
return false;
}
const scene = this.scene;
const renderer = scene.renderer;
if (!renderer || !renderer.gl) {
// Canvas fallback: no GLSL, no composite — the filters are skipped
// (the content sub-effects still render).
return false;
}
// The UI camera split must exist before the first filtered pass:
// main draws the world (and the filters act on it); fx-ui draws the
// HUD on top, unfiltered.
if (!ensureUiCameras(scene)) {
console.warn('[orbit] UI camera split failed — the effect would warp the HUD; rendering none.');
return false;
}
// Per-system variation (SystemGenerator stamps content.fx).
this.phase = Number(scene.systemContent?.fx?.phase) || 0;
this.flare = eff.flare && eff.flare.interval ? eff.flare : null;
const cam = scene.cameras.main;
let attached = false;
// --- ripple (when the bundle has one) ---------------------------------
if (hasRipple && ensureRippleNode(renderer)) {
const c = new RippleController(cam);
c.strength = num(ripple, 'strength', 18);
c.baseAmp = num(ripple, 'amplitude', 0.004);
c.speed = num(ripple, 'speed', 1);
c.anchor = ripple.center === 'star' ? 'star' : 'screen';
c.centers = num(ripple, 'centers', 1) >= 2 ? 2 : 1;
c.orbit =
c.centers === 2 && ripple.orbit
? { radius: num(ripple.orbit, 'radius', 0.16), period: num(ripple.orbit, 'period', 36) }
: null;
c.flare = this.flare ? { rippleBoost: num(this.flare, 'rippleBoost', 0) } : null;
c.phase = this.phase;
// padding must cover the worst-case displacement: baseAmp, boosted by
// the flare, summed over the active centers, across the widest side.
const boost = this.flare ? 1 + num(this.flare, 'rippleBoost', 0) : 1;
const centers = c.centers;
const maxDim = Math.max(scene.scale.width, scene.scale.height);
const need = c.baseAmp * boost * centers * maxDim;
const pad = Math.max(4, Math.ceil(num(ripple, 'padding', 20)), Math.ceil(need));
c.setPaddingOverride(-pad, -pad, pad, pad);
// Initial center(s) (update() refines every frame).
if (c.centers === 2 && c.orbit) {
const o = orbitCenters(0, c.orbit.period, c.orbit.radius, c.phase);
c.cx0 = o.cx0; c.cy0 = o.cy0; c.cx1 = o.cx1; c.cy1 = o.cy1;
c.amp0 = c.baseAmp; c.amp1 = c.baseAmp;
} else if (c.anchor === 'star') {
const m = cam?.matrixCombined;
if (m && cam.width > 0 && cam.height > 0) {
const uv = worldToUV(m, cam.width, cam.height, 0, 0);
c.cx0 = uv.x; c.cy0 = uv.y;
} else {
c.cx0 = 0.5; c.cy0 = 0.5;
}
c.amp0 = c.baseAmp; c.amp1 = 0;
} else {
c.cx0 = 0.5; c.cy0 = 0.5; c.amp0 = c.baseAmp; c.amp1 = 0;
}
cam.filters.internal.add(c);
this.controller = c;
attached = true;
}
// --- grade (when the bundle has one) ---------------------------------
if (eff.grade && ensureGradeNode(renderer)) {
const g = new GradeController(cam);
const gc = eff.grade;
g.uBright = num(gc, 'brightness', 1);
g.uSat = num(gc, 'saturation', 1);
if (gc.tint) {
const [r, gr, b] = tintMultiplier(gc.tint);
g.uTintR = r; g.uTintG = gr; g.uTintB = b;
g.uTintAmt = num(gc, 'amount', 0.3);
}
// Shadow lift (the red void) — lift only the darkness toward a hue;
// and the gritty grain that gives the haze its texture (shader-only).
if (gc.lift) {
const [lr, lg, lb] = hexToRgb01(gc.lift.color ?? '#c0392b');
g.uLiftR = lr; g.uLiftG = lg; g.uLiftB = lb;
g.uLiftAmt = num(gc.lift, 'amount', 0.18);
}
if (gc.grain) {
g.uGrainAmt = num(gc.grain, 'amount', 0);
g.grainParallax = num(gc.grain, 'parallax', 0);
}
if (gc.split) {
const [ar, ag, ab] = tintMultiplier(gc.split.a ?? '#ffffff');
const [br, bg, bb] = tintMultiplier(gc.split.b ?? '#ffffff');
g.uSplitAR = ar; g.uSplitAG = ag; g.uSplitAB = ab;
g.uSplitBR = br; g.uSplitBG = bg; g.uSplitBB = bb;
g.uSplitMix = num(gc.split, 'mix', 0.15);
const deg = num(gc.split, 'axis', 0);
const a = (deg * Math.PI) / 180;
// seed the split axis per system (angle 0..1 → 0..2π) so each
// binary's two light sources come from its own direction.
const ax2 = Math.cos(a + this.phase * Math.PI * 2);
const ay2 = Math.sin(a + this.phase * Math.PI * 2);
g.uAxisX = ax2;
g.uAxisY = ay2;
}
if (this.flare) {
const [fr, fg, fb] = hexToRgb01(this.flare.flashColor ?? '#ffffff');
g.uFlashR = fr; g.uFlashG = fg; g.uFlashB = fb;
g.flareFlash = num(this.flare, 'flash', 0.3);
}
cam.filters.internal.add(g);
this.grade = g;
attached = true;
}
this.kind =
hasRipple && hasGrade ? 'ripple+grade' : hasRipple ? 'ripple' : 'grade';
return attached;
}
/**
* Per-frame advance (call with the game-loop time, ms):
* - the ripple phase, its center(s) (orbit, or the star's screen UV),
* and — during a flare — the amplitude surge;
* - the grade's flare flash (same shared flare signal).
*
* @param {number} nowMs game-loop time (ms)
*/
update(nowMs) {
// The shared flare (0..1), computed once for both filters.
const fl = this.flare;
const flareVal = fl
? flareIntensity(nowMs, fl.interval, fl.duration, this.phase * (Number(fl.interval) || 18))
: 0;
const c = this.controller;
if (c) {
c.time = ripplePhase(nowMs, c.speed);
const amp = c.baseAmp * (1 + (c.flare?.rippleBoost ?? 0) * flareVal);
if (c.centers === 2 && c.orbit) {
const o = orbitCenters(nowMs, c.orbit.period, c.orbit.radius, c.phase);
c.cx0 = o.cx0; c.cy0 = o.cy0; c.cx1 = o.cx1; c.cy1 = o.cy1;
c.amp0 = amp; c.amp1 = amp;
} else {
if (c.anchor === 'star') {
const cam = this.scene.cameras.main;
const m = cam?.matrixCombined;
if (m && cam.width > 0 && cam.height > 0) {
const uv = worldToUV(m, cam.width, cam.height, 0, 0);
c.cx0 = uv.x; c.cy0 = uv.y;
}
}
c.amp0 = amp; c.amp1 = 0;
}
}
const g = this.grade;
if (g) {
g.uFlash = g.flareFlash * flareVal;
// Grain parallax: offset the grain by the camera scroll so it shifts
// slightly as you fly (sits at a depth, not on the lens).
if (g.grainParallax > 0) {
const cam = this.scene.cameras.main;
g.uGrainOffX = (cam?.scrollX ?? 0) * g.grainParallax;
g.uGrainOffY = (cam?.scrollY ?? 0) * g.grainParallax;
}
}
}
/** Detach and destroy the current effect (safe to call repeatedly). */
release() {
for (const c of [this.controller, this.grade]) {
if (!c) continue;
const list = c?.camera?.filters?.internal;
if (list?.remove) list.remove(c);
else if (typeof c?.destroy === 'function') c.destroy();
}
this.controller = null;
this.grade = null;
this.kind = null;
this.flare = null;
}
}