TRACE THE READ PATH
A stored or displayed number is not an effect until production code consumes it
Use four questions for every stat: where is it stored, where is it decoded, which reachable function reads it, and what concrete outcome changes? If the chain stops at a card or description, the value is presentation. If a browser reads it only for preview, it is not contract settlement.
PlayerSkills solves the source-of-level question: all ten XP totals and levels come from one owner-scoped PDA and one SkillRuleTable. It does not automatically make every designed effect active. Each system still needs an explicit consumer.
Contract consumers also need identity checks. Chunk, Smelting, and Backpack derive expected PlayerSkills, validate Skills owner, layout, wallet owner, GlobalConfig, and level range, then use only the relevant level.
PlayerProfile, PlayerSkills, rule table, Backpack, or browser state.
Wrong program owner or PDA must fail closed.
A description or test helper is not enough.
Movement speed, resource volume, drop weighting, material output, grade, or durability.
PLAYERPROFILE DEFAULTS
Six PlayerProfile attributes are stored defaults, not the ten-skill source
PlayerProfile version 7 initializes health 100, energy 100, stamina 100, mining power 1, build power 1, and defense 0. Those bytes remain schema fields, but Profile skill cards do not decode them.
Profile derives overview level from the average of ten PlayerSkills levels and six visible stat cards from skill definitions. Chunk mining output, Smelting, Forging, and movement do not read the six defaults.
Do not equate PlayerProfile stamina with the Stamina skill, or mining power with Precision Gathering. Similar labels are separate until a consumer explicitly connects them.
Initialized in PlayerProfile and not skill-level inputs.
Stored defaults with no current effect loop identified here.
Source for Profile cards and active consumers.
A browser presentation derived from chain levels.
CHAIN LEVELS, BROWSER PRESENTATION
The browser derives cards and progress from chain levels, XP, and thresholds
Profile builds one skill state from PlayerSkills XP, PlayerSkills levels, and SkillRuleTable thresholds. It normalizes those values, calculates tier progress from the cumulative table, and uses definitions to format card text.
The six overview cards show Gathering, Carry, extra Smelting output, Speed, Strength, and Crafting. These are presentation formulas over authoritative levels. Enforcement status depends on the consumer matrix, not on accurate arithmetic.
A missing PlayerSkills account produces zero levels until synchronization creates it. A malformed present account is rejected. There is no local XP fallback or maximum merge.
Ten u64 totals decoded from one owner PDA.
Ten u8 values recomputed by Skills.
Strictly increasing cumulative values.
Presentation parameters do not replace contract formulas.
CURRENT CONSUMER MATRIX
Four contract effects, one local movement effect, and five planned effect loops
Precision Gathering is consumed by browser mining preview and Chunk's authoritative gathered-volume calculation. Exploration is consumed by Chunk's deterministic extra-drop weighting. Smelting is consumed by preview and Smelting's physical output calculation. Forging is consumed by Backpack's durability bonus after material-derived grade and item level establish the base.
Swiftness is consumed by local movement controls: walking speed is 14.8 times the skill multiplier and running remains exactly twice walking. Guardian receives pose updates but does not authorize that multiplier.
Burden has an enforced XP earning rule based on pre-mine Backpack mass, but displayed safe-carry kilograms do not cap inventory or movement. Craftsmanship tier, Stamina fatigue reduction, Strength one-hand mass, and Appraisal traits are displayed parameters without consumers.
Authoritative gathered volume and extra-drop weighting.
Authoritative material output and verified forge outcome.
Chain level, local speed consumer.
XP may be authoritative while displayed effect lacks enforcement.
PRECISION AND EXPLORATION
Mining previews locally, then Chunk recomputes authoritative volume and extra drops
Mining reads the current Precision effect to build a pending reward preview. This improves responsiveness but remains reversible until chain confirmation.
Chunk independently validates PlayerSkills and reads Precision and Exploration levels. Precision selects gathered volume. Exploration changes deterministic extra-drop weighting under ResourceDropTable. The browser cannot submit an accepted yield or rarity result as authority.
After confirmation, local state removes only committed blocks and Backpack refresh supplies stored resources. Failure restores pending state.
Used for immediate local presentation.
Chunk reads levels from chain bytes.
Contract-selected from authoritative level.
Applied inside deterministic selection.
GAME CONTRACT CONSUMERS
Smelting and Forging read PlayerSkills but compute accepted output inside their programs
Smelting validates owner-derived PlayerSkills and reads Smelting level. It applies 10,000 + 500 × level basis points, capped at 15,000, to each non-merge output's physical volume and integer stack quantity. The browser previews with the same level, but inputs, fuel, output quantity, volume, and capacity are revalidated on chain. Material-merge recipes preserve all consumed quantity and volume instead of applying the skill multiplier.
Verified Forging validates PlayerSkills and reads Forging level. Backpack first combines material grade, item level, weak-material caps, quality, and raw durability into a capped base, then applies exactly +5% durability per Forging level, capped at level 10. The Profile card describes that final level factor, not the complete material calculation.
Both programs fail closed on substituted identity. A missing account is level zero; a present malformed account is rejected.
Exactly +5 percentage points per level, enforced on non-merge output volume and integer stack quantity.
Level multiplies the capped material-derived base by 1 + 0.05 × min(level, 10).
Allows base behavior before account creation.
Wrong identity, layout, GlobalConfig, or level fails.
CHAIN-BACKED LEVEL, LOCAL CONSUMER
Swiftness changes browser movement speed, while running stays exactly two times walking
The level source is PlayerSkills, including +1 XP for proven mining coordinates at least 160 blocks apart. The consumer remains local browser controls.
playerMovementSpeeds multiplies base walking speed 14.8 by 1 + 0.03 × level, capped at 1.3, then defines running as walking × 2. applyPlayerMovementSpeeds writes both values to controls.
Guardian packets report movement pose for realtime presence. Guardian does not receive PlayerSkills, calculate the multiplier, or settle movement distance. This is a local effect backed by a chain-authoritative level.
Multiplied by Swiftness effect.
Level 10 gives a 1.3 multiplier.
Uses the same skill-adjusted base.
Not a level or speed authority.
CURRENT BOUNDARIES
Guardian does not fill missing stat consumers, and several effect loops remain unfinished
Guardian handles region connection, nearby pose, chat, and verified building discovery. Its movement protocol does not carry ten skill levels, PlayerProfile defaults, Backpack mass, Smelting multiplier, or Forging formula.
No current path enforces displayed Burden capacity, Craftsmanship process tier, Stamina fatigue multiplier, Strength one-hand limit, or Appraisal trait count. These can remain product targets, but UI and documentation must not present them as enforced.
Completion requires an explicit consumer, PlayerSkills identity validation at the trust boundary, and tests proving changed outcome. Do not infer enforcement from a card, source coefficient, or authoritative XP balance.
Realtime service data with separate checks.
It cannot make a planned card enforced.
No changed production outcome found.
All three are needed before claiming enforcement.
READ THE IMPLEMENTATION
Ten equations and thirteen literal excerpts separate storage, presentation, local effects, and settlement
Each claim identifies a concrete reader and changed outcome. Excerpts are continuous ranges from current source, and missing consumers are stated instead of inferred.
The consumer test
Effect(v, a) exists only if reachable production code reads v and changes outcome(a)Storage, decoding, and display are not gameplay enforcement.
PlayerSkills level source
level[s] = PlayerSkills.levels[index(s)]All ten Profile cards share this source.
Profile overview level
ProfileLevel = max(1, 1 + floor(sum(ten skill levels) / 10))This is a browser summary, not another ledger.
Precision preview
previewVolume = floor(1,000 cm³ × PrecisionBps / 10,000)Chunk independently recomputes the accepted resource volume on chain.
Smelting output multiplier
SmeltingBps = min(15,000, 10,000 + 500 × level)Implemented in preview and contract settlement.
Swiftness walking speed
walk = 14.8 × min(1.3, 1 + 0.03 × level)The browser applies this control effect.
Running speed
run = walk × 2Swiftness changes the shared base, not sprint ratio.
Forging material grade
grade = min(10, round((3×materialGrade + itemLevelGrade)/4), materialGrade + 1, weakGradeCap)Forging level does not change grade in the current settlement formula.
Forging durability factors
base = min(effectiveDurability × (90 + 5×grade) × (100 + itemLevel/2) / 10,000, 1.05×rawDurability); durability = floor(base × (100 + 5×min(level,10)) / 100)The current contract applies the displayed five-percent-per-level factor after the material-derived base and cap.
Missing-consumer boundary
Displayed parameter + authoritative level ≠ enforced effectThis applies to several planned effects.
Production Play loads play/main.js
HTMLplay/index.html <script type="module" src="./main.js"></script>
Consumer claims start from the current production entry.
- Line 1
Load the modular game entry for /play/.
PlayerProfile declares six defaults
Rustprograms/nicechunk_player/src/state.rspub const DEFAULT_HEALTH: u16 = 100;
pub const DEFAULT_ENERGY: u16 = 100;
pub const DEFAULT_STAMINA: u16 = 100;
pub const DEFAULT_MINING_POWER: u16 = 1;
pub const DEFAULT_BUILD_POWER: u16 = 1;
pub const DEFAULT_DEFENSE: u16 = 0;
These constants describe Profile initialization, not skill levels.
- Constants
Define health, energy, stamina, mining power, build power, and defense.
Profile accepts chain skill state
JavaScriptplay/play-profile-ui.js const skillState = buildProfileSkillState({
chainXp: chain.playerSkillXp || chain.skillXp || null,
chainLevels: chain.playerSkillLevels || chain.skillLevels || null,
chainThresholds: chain.playerSkillThresholds || chain.skillThresholds || null,
});
UI passes PlayerSkills XP, levels, and SkillRuleTable thresholds.
- Inputs
Select chain values from Player snapshot.
- Boundary
No local behavior profile participates.
Overview derives from authoritative levels
JavaScriptplay/play-profile-ui.js function renderOverview(profile, chain, skillState) {
const [px, , pz] = getPlayerPosition?.() ?? [0, 0, 0];
const chainBackpack = chain.chainBackpack || {};
const levels = effectiveSkillLevels(skillState);
const totalSkillLevels = Object.values(levels).reduce((sum, value) => sum + value, 0);
const level = Math.max(1, 1 + Math.floor(totalSkillLevels / Math.max(1, PLAYER_SKILL_DEFINITIONS.length)));
const reputation = Math.max(0,
Number(profile.confirmedMines || 0) * 5
+ Number(profile.confirmedPlacements || 0) * 3
+ Number(profile.materialsSmelted || 0) * 4,
);
const chunkX = Math.floor(px / 16);
const chunkZ = Math.floor(pz / 16);
const regionX = Math.floor(chunkX / 100);
const regionZ = Math.floor(chunkZ / 100);
const backpackValue = chainBackpack.backpackAddress
? `${chainBackpack.syncedSlots || 0} / ${chainBackpack.capacity || 0}`
: ui("main.profile.noBackpack", "No backpack");
const balance = chain.walletAddress && Number.isFinite(Number(chain.walletBalanceSol))
? `${formatSol(chain.walletBalanceSol)} SOL`
: ui("main.profile.balanceUnavailable", "0 SOL");
setText(elements.profileIdentityBalance, balance);
setText(elements.profileLevelValue, String(level));
setText(elements.profileReputationValue, reputation.toLocaleString());
setText(elements.profileRegionValue, `NiceChunk / ${regionX}, ${regionZ}`);
setText(elements.profileBackpackValue, backpackValue);
renderStats(skillState);
}
Overview level is a presentation formula over PlayerSkills levels.
- Level
Average ten chain levels for visible Level.
- Identity
Render wallet, region, and Backpack separately.
Runtime effects require explicit chain levels
JavaScriptplay/play-skill-effects.jsexport function createProfileSkillEffects({
chainXp = null,
chainLevels = null,
} = {}) {
const state = buildProfileSkillState({ chainXp, chainLevels });
const level = (skillId) => {
const skill = SKILL_BY_ID[skillId];
return skill ? profileSkillStateLevel(state, skill) : 0;
};
const levels = Object.fromEntries(PLAYER_SKILL_DEFINITIONS.map((skill) => [skill.id, level(skill.id)]));
const effects = Object.fromEntries(PLAYER_SKILL_DEFINITIONS.map((skill) => [
skill.effect.key,
profileSkillEffectValue(skill, levels[skill.id]),
]));
return {
levels,
...effects,
};
}
Runtime does not infer a level from local XP or behavior.
- State
Build normalized state from supplied chain values.
- Effects
Evaluate each definition at authoritative level.
Chunk validates Precision and Exploration
Rustprograms/nicechunk_chunk/src/lib.rs let precision_level = player_skill_level(
player_skills,
global_config.key,
&context.owner,
PRECISION_GATHERING_SKILL_INDEX,
)?;
let exploration_level = player_skill_level(
player_skills,
global_config.key,
&context.owner,
EXPLORATION_SKILL_INDEX,
)?;
Ok((
PlayerProgressState::precision_gathering_volume_mm3_from_level(precision_level),
exploration_level,
))
}
fn player_skill_level(
player_skills: &AccountInfo,
global_config: &Pubkey,
owner: &Pubkey,
skill_index: usize,
) -> Result<u8, solana_program::program_error::ProgramError> {
let (expected, _) = Pubkey::find_program_address(
&[PLAYER_SKILLS_SEED, global_config.as_ref(), owner.as_ref()],
&NICECHUNK_SKILLS_PROGRAM_ID,
);
require_key_eq(
player_skills.key,
&expected,
NicechunkChunkError::InvalidPlayerSkillsPda,
)?;
if player_skills.owner == &system_program::ID && player_skills.data_len() == 0 {
return Ok(0);
}
require_key_eq(
player_skills.owner,
&NICECHUNK_SKILLS_PROGRAM_ID,
NicechunkChunkError::InvalidPlayerSkillsOwner,
)?;
let data = player_skills.try_borrow_data()?;
if data.len() != PLAYER_SKILLS_LEN
|| data[0..8] != PLAYER_SKILLS_MAGIC
|| u16::from_le_bytes([data[8], data[9]]) != PLAYER_SKILLS_VERSION
|| data[11] != 1
|| &data[PLAYER_SKILLS_OWNER_OFFSET..PLAYER_SKILLS_OWNER_OFFSET + 32] != owner.as_ref()
|| &data[PLAYER_SKILLS_GLOBAL_CONFIG_OFFSET..PLAYER_SKILLS_GLOBAL_CONFIG_OFFSET + 32]
!= global_config.as_ref()
{
return Err(NicechunkChunkError::InvalidPlayerSkillsData.into());
}
let level = data
.get(PLAYER_SKILLS_LEVELS_OFFSET + skill_index)
.copied()
.ok_or(NicechunkChunkError::InvalidPlayerSkillsData)?;
if level > MAX_SKILL_LEVEL {
return Err(NicechunkChunkError::InvalidPlayerSkillsData.into());
}
Ok(level)
}
Chunk derives and validates PlayerSkills before mining effects.
- Load
Read two skill indexes.
- Identity
Check PDA, owner, layout, wallet, GlobalConfig, and range.
Browser mining creates only a preview
JavaScriptplay/mining-controller.js const resourceId = Number.isFinite(hit.resourceId) ? hit.resourceId : def.resourceId;
const txId = `local-pending-${txSerial++}`;
const skillEffects = getSkillEffects?.() ?? {};
const yieldBps = Math.max(1, Math.min(10000, Math.trunc(skillEffects.precisionGatheringBps || 5000)));
const volumeMilliLiters = Math.max(1, Math.floor(1000 * yieldBps / 10000));
const planBlocks = currentPlanBlocks(swing, hit);
const plannedRewardBlocks = swing.miningPlan?.rewardBlocks?.length ? swing.miningPlan.rewardBlocks : planBlocks;
const rewardGroups = rewardGroupsForBlocks(uniqueMiningBlocks([hit, ...plannedRewardBlocks]), {
yieldBps,
volumeMilliLiters,
});
Pending reward groups use Precision for responsiveness.
- Effect
Read Precision basis points.
- Preview
Build local volume and rewards that remain pending.
Smelting consumes authoritative level
Rustprograms/nicechunk_smelting/src/lib.rs let smelting_level = player_skill_level(player_skills, global_config.key, owner.key)?;
let skill_output_bps = PlayerProgressState::smelting_output_bps_from_level(smelting_level);
let validated_inputs = {
let backpack_data = backpack.try_borrow_data()?;
BackpackAccountView::validate_recipe_inputs(
&backpack_data,
owner.key,
indexes,
fuel_indexes,
&recipe,
multiplier,
)?
};
let recipe_input_volume_mm3 = recipe_input_volume_mm3(&recipe);
let mut output_volumes_mm3 = [0_u32; state::RECIPE_MAX_OUTPUTS];
let mut output_quantities = [0_u32; state::RECIPE_MAX_OUTPUTS];
for output_index in 0..recipe.output_count as usize {
let output = &recipe.outputs[output_index];
output_volumes_mm3[output_index] = smelting_recipe_output_volume_mm3(
&recipe,
output,
validated_inputs.input_volume_mm3,
recipe_input_volume_mm3,
multiplier,
skill_output_bps,
)?;
output_quantities[output_index] = smelting_output_quantity(
&recipe,
output,
validated_inputs.consumed_input_units,
multiplier,
skill_output_bps,
)?;
}
The program reads the authoritative PlayerSkills level and validates every resulting output volume and quantity before it consumes any Backpack input.
- Level
Decode Smelting level.
- Output
Apply the level-derived basis points while precomputing checked output volume and quantity arrays.
Smelting is zero to fifty percent extra
Rustprograms/nicechunk_smelting/src/state.rs pub fn smelting_output_bps_from_level(level: u8) -> u16 {
let level = u16::from(level.min(10));
SMELTING_SKILL_BASE_OUTPUT_BPS
.saturating_add(level.saturating_mul(SMELTING_SKILL_OUTPUT_BPS_PER_LEVEL))
.min(SMELTING_SKILL_MAX_OUTPUT_BPS)
}
Contract formula matches current browser Smelting metric.
- Clamp
Limit level to ten.
- Basis points
Add 500 per level and cap at 15,000.
Forging builds a material base, then applies the level bonus
Rustprograms/nicechunk_backpack/src/state.rs let material_level =
material_item_level_from_durability(total_effective_durability, total_volume);
let item_level = material_level.max(1);
let item_level_grade = 1_u8.saturating_add((item_level.saturating_sub(1) / 10).min(9));
let blended_grade = ((material_grade as u16 * 3 + item_level_grade as u16 + 2) / 4) as u8;
let grade = blended_grade
.max(1)
.min(10)
.min(material_grade.saturating_add(1).min(10))
.min(weak_grade_cap);
let material_factor = 90_u64.saturating_add(grade as u64 * 5);
let level_factor = 100_u64.saturating_add(item_level as u64 / 2);
let base_candidate = total_effective_durability
.saturating_mul(material_factor)
.saturating_mul(level_factor)
/ 10_000;
let base_material_cap = total_raw_durability.saturating_mul(105) / 100;
let base_durability = base_candidate.max(1).min(base_material_cap.max(1));
let durability_bonus_percent = 100_u64.saturating_add(forging_level.min(10) as u64 * 5);
let durability_max = base_durability
.saturating_mul(durability_bonus_percent)
.saturating_div(100)
.max(1)
.min(u32::MAX as u64) as u32;
ForgeOutcome {
grade,
item_level,
durability_max,
quality_bps,
volume_mm3: total_volume.max(1).min(u32::MAX as u64) as u32,
mass_grams: 0,
gained_xp: 1,
}
Backpack derives grade and base durability from materials, applies exactly five percent per verified Forging level up to level ten, and awards one XP for every completed forge.
- Grade
Blend material and item-level grade, then apply material-derived caps.
- Durability
Cap the material-derived base, then apply the five-percent-per-level multiplier.
- XP
Award one XP for the completed forging action.
Swiftness changes controls and keeps two-times sprint
JavaScriptplay/play-movement-speed.jsexport const PLAYER_MOVEMENT_CONFIG = Object.freeze({
baseSpeed: 14.8,
sprintMultiplier: 2,
});
export function playerMovementSpeeds(movementSpeedMultiplier = 1) {
const parsed = Number(movementSpeedMultiplier);
const skillMultiplier = Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
const walking = PLAYER_MOVEMENT_CONFIG.baseSpeed * skillMultiplier;
return {
walking,
running: walking * PLAYER_MOVEMENT_CONFIG.sprintMultiplier,
};
}
export function applyPlayerMovementSpeeds(controls, skillEffects = {}) {
const speeds = playerMovementSpeeds(skillEffects.movementSpeedMultiplier);
if (controls) {
controls.speed = speeds.walking;
controls.sprintMultiplier = PLAYER_MOVEMENT_CONFIG.sprintMultiplier;
}
return speeds;
}
The chain-backed level reaches controls.speed and sprintMultiplier.
- Config
Set base 14.8 and sprint 2.
- Calculation
Apply skill to walking and derive running.
- Consumer
Write values to controls.
Guardian MOVE contains pose, not skills
JavaScriptplay/play-guardian-client.js encodeMove({ x, y, z, yaw, pitch }) {
const chunk = this.worldToChunk(x, z);
const local = this.globalToLocalChunk(chunk.x, chunk.z);
const offsetX = x - chunk.x * this.chunkSize;
const offsetZ = z - chunk.z * this.chunkSize;
const bytes = new Uint8Array(MOVE_SIZE);
const view = new DataView(bytes.buffer);
view.setUint8(0, MSG_MOVE);
view.setUint8(1, clampByte(local.x));
view.setUint8(2, clampByte(local.z));
view.setUint16(3, clampU16(Math.round(offsetX * this.positionPrecision)), true);
view.setUint16(5, clampU16(Math.round(y * this.positionPrecision)), true);
view.setUint16(7, clampU16(Math.round(offsetZ * this.positionPrecision)), true);
view.setUint8(9, encodeYaw(yaw));
view.setInt8(10, encodePitch(pitch));
view.setUint16(11, ++this.clientTick & 0xffff, true);
return bytes;
}
Realtime movement payload has pose fields but no PlayerSkills or stat table.
- Position
Encode chunk-local x, y, and z.
- Orientation
Encode yaw and pitch.
- Ordering
Include a client tick, not skill state.
PlayerSkills presentation has no XP merge
JavaScriptplay/play-profile-skills.jsexport function buildProfileSkillState({
chainXp = null,
chainLevels = null,
chainThresholds = null,
} = {}) {
const levels = normalizeSkillLevels(chainLevels);
const xpBySkill = normalizeSkillXp(chainXp);
const thresholdsBySkill = normalizeSkillThresholds(chainThresholds);
return {
levels,
xpBySkill,
thresholdsBySkill,
resolvedLevels: { ...levels },
source: "chain",
};
}
Final state normalizes only chain-supplied values.
- Inputs
Accept chain XP, levels, and thresholds.
- Result
Return one chain-sourced state.
IMPLEMENTATION EVIDENCE
Where these claims come from
Each claim is intentionally scoped to a concrete implementation path. These references are for verification, not decoration.
play/index.html
Loads current modular production game.
programs/nicechunk_player/src/state.rs
Defines six PlayerProfile defaults and version-7 layout.
play/play-profile-ui.js
Builds overview and cards from PlayerSkills.
play/play-profile-skills.js
Defines presentation parameters and chain-only state.
play/play-skill-effects.js
Creates runtime effects from explicit levels.
play/play-movement-speed.js
Applies Swiftness and two-times running.
play/mining-controller.js
Uses Precision for reversible preview.
src/chain/nicechunkChain.js
Validates PlayerSkills and SkillRuleTable.
programs/nicechunk_chunk/src/lib.rs
Consumes Precision and Exploration.
programs/nicechunk_chunk/src/state.rs
Defines gathered volume and Exploration logic.
programs/nicechunk_smelting/src/lib.rs
Consumes the authoritative Smelting level during settlement.
programs/nicechunk_smelting/src/state.rs
Defines the enforced Smelting output multiplier.
programs/nicechunk_backpack/src/lib.rs
Validates and reads Forging level.
programs/nicechunk_backpack/src/state.rs
Defines the multi-factor verified Forging outcome.
programs/nicechunk_skills/src/state.rs
Stores ten authoritative XP totals and levels.
play/play-guardian-client.js
Defines Guardian pose payload without skill state.
Guardian/src/protocol.h
Defines Guardian protocol independently from Skills.
play/tests/play-skill-movement.test.mjs
Checks authoritative movement and two-times running.
play/tests/smelting-skill-output.test.mjs
Checks Smelting preview levels.
tests/nicechunk_skills.ts
Checks current Skills account and instruction structure.