From 1bdf884b83db89bcc55fb127be78b39ff26cf518 Mon Sep 17 00:00:00 2001 From: Hanke Chen Date: Fri, 11 Jul 2025 16:45:10 -0400 Subject: [PATCH] :sparkles: consider weights and OR of variants --- src/render/BlockDefinition.ts | 47 ++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/render/BlockDefinition.ts b/src/render/BlockDefinition.ts index f9c15291..bbcdeabc 100644 --- a/src/render/BlockDefinition.ts +++ b/src/render/BlockDefinition.ts @@ -18,7 +18,8 @@ type ModelVariantEntry = ModelVariant | (ModelVariant & { })[] type ModelMultiPartCondition = { - OR: ModelMultiPartCondition[], + OR?: ModelMultiPartCondition[], + AND?: ModelMultiPartCondition[], } | { [key: string]: string, } @@ -43,10 +44,10 @@ export class BlockDefinition { const matches = Object.keys(this.variants).filter(v => this.matchesVariant(v, props)) if (matches.length === 0) return [] const variant = this.variants[matches[0]] - return [Array.isArray(variant) ? variant[0] : variant] + return [this.weightedApply(variant)] } else if (this.multipart) { const matches = this.multipart.filter(p => p.when ? this.matchesCase(p.when, props) : true) - return matches.map(p => Array.isArray(p.apply) ? p.apply[0] : p.apply) + return matches.map(p => this.weightedApply(p.apply)) } return [] } @@ -80,6 +81,41 @@ export class BlockDefinition { return mesh.transform(t) } + private weightedApply(apply: ModelVariantEntry) { + if (Array.isArray(apply)) { + // Sets the probability of the model for being used in the game, + // defaults to 1 (=100%). If more than one model is used for the same + // variant, the probability is calculated by dividing the individual + // model's weight by the sum of the weights of all models. + // (For example, if three models are used with weights 1, 1, and 2, + // then their combined weight would be 4 (1+1+2). The probability of each + // model being used would then be determined by dividing each weight + // by 4: 1/4, 1/4 and 2/4, or 25%, 25% and 50%, respectively.) + + const totalWeight: number = apply + .reduce((sum, entry) => sum + (entry.weight ?? 1), 0); + let r: number = Math.random() * totalWeight; + + // Iterate through the entries, subtracting weight until we find the selected one + for (const entry of apply) { + const w: number = entry.weight ?? 1; + if (r < w) { + // Destructure to drop the weight property and return the variant + const { weight, ...variant }: { weight?: number } & ModelVariant = entry; + return variant; + } + r -= w; + } + + // Fallback (due to floating-point edge cases): return the last variant + const lastEntry = apply[apply.length - 1]; + const { weight, ...variant }: { weight?: number } & ModelVariant = lastEntry; + return variant; + } else { + return apply + } + } + private matchesVariant(variant: string, props: { [key: string]: string }): boolean { return variant.split(',').every(p => { const [k, v] = p.split('=') @@ -88,9 +124,12 @@ export class BlockDefinition { } private matchesCase(condition: ModelMultiPartCondition, props: { [key: string]: string }): boolean { - if (Array.isArray(condition.OR)) { + if (condition.OR && Array.isArray(condition.OR)) { return condition.OR.some(c => this.matchesCase(c, props)) + } else if (condition.AND && Array.isArray(condition.AND)) { + return condition.AND.every(c => this.matchesCase(c, props)) } + const states = condition as {[key: string]: string} return Object.keys(states).every(k => { const values = states[k].split('|')