miijs 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,123 @@
1
+ // ------------------------------
2
+ // Helpers
3
+ // ------------------------------
4
+ const clamp = (v, min, max) => Math.min(max, Math.max(min, v));
5
+
6
+ // ------------------------------
7
+ // Height (0..127) <-> feet/inches
8
+ // Calibrated so height=64 -> 67 inches exactly.
9
+ // Range: 36" (3'0") .. 84" (7'0").
10
+ // ------------------------------
11
+ const HEIGHT_MIN_IN = 36;
12
+ const HEIGHT_MAX_IN = 84;
13
+ const HEIGHT_RANGE_IN = HEIGHT_MAX_IN - HEIGHT_MIN_IN;
14
+
15
+ // Calibration target: slider mid (64) should be 67"
16
+ const HEIGHT_MID_SLIDER = 64;
17
+ const HEIGHT_MID_IN = 67;
18
+
19
+ // Compute exponent k so that x0^k = y0 (bias curve, invertible)
20
+ const x0 = HEIGHT_MID_SLIDER / 127; // ~0.50394
21
+ const y0 = (HEIGHT_MID_IN - HEIGHT_MIN_IN) / HEIGHT_RANGE_IN; // 31/48 ≈ 0.64583
22
+ const HEIGHT_EXP = Math.log(y0) / Math.log(x0); // ≈ 0.638 (computed once)
23
+
24
+ /** 0..127 -> { feet, inches, totalInches } */
25
+ function miiHeightToFeetInches(heightValue) {
26
+ const hv = clamp(heightValue | 0, 0, 127);
27
+ const x = hv / 127;
28
+ const s = Math.pow(x, HEIGHT_EXP); // calibrated bias
29
+ const inches = HEIGHT_MIN_IN + s * HEIGHT_RANGE_IN;
30
+
31
+ const totalInches = Math.round(inches);
32
+ const feet = Math.floor(totalInches / 12);
33
+ const inchesR = totalInches % 12;
34
+
35
+ return { feet, inches: inchesR, totalInches };
36
+ }
37
+
38
+ /** inches -> 0..127 (inverse of the calibrated mapping) */
39
+ function inchesToMiiHeight(targetInches) {
40
+ const y = clamp((targetInches - HEIGHT_MIN_IN) / HEIGHT_RANGE_IN, 0, 1);
41
+ const x = Math.pow(y, 1 / HEIGHT_EXP);
42
+ return Math.round(x * 127);
43
+ }
44
+
45
+ // ------------------------------
46
+ // Weight (0..127) + height(0..127) <-> pounds
47
+ // Baseline by BMI; composition slider is nonlinear & invertible.
48
+ // Center (64) -> multiplier 1.0.
49
+ // ------------------------------
50
+ const BMI_AVG = 23.5; // gives ~150 lb at 67"
51
+ const WEIGHT_SPREAD = 0.35; // ±35% around baseline at extremes
52
+ const WEIGHT_GAMMA = 1.6; // steeper near ends, softer center
53
+
54
+ /** weight slider + height slider -> pounds */
55
+ function miiWeightToPounds(weightValue, heightValue) {
56
+ const wv = clamp(weightValue | 0, 0, 127);
57
+ const hv = clamp(heightValue | 0, 0, 127);
58
+
59
+ // Height in inches using the calibrated mapping
60
+ const { totalInches: H } = miiHeightToFeetInches(hv);
61
+
62
+ // BMI baseline
63
+ const base = (BMI_AVG * H * H) / 703;
64
+
65
+ // Nonlinear symmetric multiplier around 1.0 (64 -> 0 offset)
66
+ const d = clamp((wv - 64) / 63, -1, 1);
67
+ const multiplier = 1 + Math.sign(d) * WEIGHT_SPREAD * Math.pow(Math.abs(d), WEIGHT_GAMMA);
68
+
69
+ return Math.round(base * multiplier);
70
+ }
71
+
72
+ /** pounds + height slider -> weight slider (inverse of above) */
73
+ function poundsToMiiWeight(pounds, heightValue) {
74
+ const hv = clamp(heightValue | 0, 0, 127);
75
+ const { totalInches: H } = miiHeightToFeetInches(hv);
76
+ const base = (BMI_AVG * H * H) / 703;
77
+
78
+ // Guard against tiny numeric drift outside the spread
79
+ const mult = clamp(pounds / base, 1 - WEIGHT_SPREAD * 1.0001, 1 + WEIGHT_SPREAD * 1.0001);
80
+
81
+ let d;
82
+ if (Math.abs(mult - 1) < 1e-9) {
83
+ d = 0;
84
+ } else if (mult > 1) {
85
+ d = Math.pow((mult - 1) / WEIGHT_SPREAD, 1 / WEIGHT_GAMMA);
86
+ } else {
87
+ d = -Math.pow((1 - mult) / WEIGHT_SPREAD, 1 / WEIGHT_GAMMA);
88
+ }
89
+
90
+ const wv = Math.round(64 + 63 * clamp(d, -1, 1));
91
+ return clamp(wv, 0, 127);
92
+ }
93
+
94
+ // ------------------------------
95
+ // Quick checks (should match your expectations)
96
+ // ------------------------------
97
+ // Average points
98
+ // console.log(miiHeightToFeetInches(64), miiWeightToPounds(64, 64)); // ~5'7", ~150 lb
99
+ // Extremes
100
+ // console.log(miiHeightToFeetInches(0), miiWeightToPounds(0, 0)); // 3'0", ~28 lb
101
+ // console.log(miiHeightToFeetInches(127), miiWeightToPounds(127, 127)); // 7'0", ~318 lb
102
+ // Inverses
103
+ // const h = miiHeightToFeetInches(64);
104
+ // console.log(inchesToMiiHeight(h.totalInches)); // ~64
105
+ // const lbs = miiWeightToPounds(96, 64);
106
+ // console.log(lbs); // ~135
107
+ // console.log(poundsToMiiWeight(lbs, 64)); // ~96
108
+
109
+
110
+ // ------------------------------
111
+ // Quick sanity checks
112
+ // ------------------------------
113
+ // Average points
114
+ console.log(miiHeightToFeetInches(64), miiWeightToPounds(64, 64)); // ~5'0", ~150 lb
115
+ // // Extremes
116
+ console.log(miiHeightToFeetInches(0), miiWeightToPounds(0, 0));//3'0, 28 lb
117
+ console.log(miiHeightToFeetInches(127), miiWeightToPounds(127, 127));//7'0 318 lb
118
+ // // Inverses
119
+ const h = miiHeightToFeetInches(64);
120
+ console.log(inchesToMiiHeight(h.totalInches)); // ~64
121
+ const lbs = miiWeightToPounds(96, 64);
122
+ console.log(lbs);//168
123
+ console.log(poundsToMiiWeight(lbs, 64)); // ~96
package/index.js CHANGED
@@ -2179,11 +2179,11 @@ function convertMii(jsonIn,typeTo){
2179
2179
  }
2180
2180
  miiTo.nose.type=convTables.nose3DSToWii[mii.nose.page][mii.nose.type];
2181
2181
  miiTo.mouth.type=convTables.mouth3DSToWii[mii.mouth.page][mii.mouth.type];
2182
- miiTo.mouth.color=mii.mouth.col>2?0:mii.mouth.col;
2182
+ miiTo.mouth.color=mii.mouth.color>2?0:mii.mouth.color;
2183
2183
  miiTo.hair.type=convTables.hair3DSToWii[mii.hair.page][mii.hair.type];
2184
2184
  miiTo.eyebrows.type=convTables.eyebrows3DSToWii[mii.eyebrows.page][mii.eyebrows.type];
2185
2185
  miiTo.eyes.type=convTables.eyes3DSToWii[mii.eyes.page][mii.eyes.type];
2186
- miiTo.glasses.col=mii.glasses.col;
2186
+ miiTo.glasses.color=mii.glasses.color;
2187
2187
  if(miiTo.beard.mustache.type===4){
2188
2188
  miiTo.beard.mustache.type=2;
2189
2189
  }
@@ -2211,7 +2211,7 @@ function convertMii(jsonIn,typeTo){
2211
2211
  }
2212
2212
  miiTo.eyes.squash=3;
2213
2213
  miiTo.eyebrows.squash=3;
2214
- miiTo.mouth.col=mouthCols3DS[mii.mouth.col];//qk
2214
+ miiTo.mouth.color=mii.mouth.color;
2215
2215
  miiTo.mouth.squash=3;
2216
2216
  miiTo.console="3ds";
2217
2217
  }
@@ -2288,9 +2288,13 @@ function convertMiiToStudio(jsonIn) {
2288
2288
  }
2289
2289
  async function readWiiBin(binOrPath) {
2290
2290
  let data;
2291
- if (/[^01]/ig.test(binOrPath)) {
2291
+ if(Buffer.isBuffer(binOrPath)){
2292
+ data=binOrPath;
2293
+ }
2294
+ else if (/[^01]/ig.test(binOrPath)) {
2292
2295
  data = await fs.promises.readFile(binOrPath);
2293
- } else {
2296
+ }
2297
+ else{
2294
2298
  data = Buffer.from(binOrPath);
2295
2299
  }
2296
2300
  var thisMii={
@@ -3054,7 +3058,7 @@ async function write3DSQR(miiJson, outPath, fflRes = getFFLRes()) {
3054
3058
  main_img.blit(canvas, 212 - 100 / 2, 212 - 100 / 2);
3055
3059
  const font = await Jimp.loadFont(Jimp.FONT_SANS_16_BLACK)
3056
3060
 
3057
- main_img.print(font, 0, 55, {
3061
+ main_img.print(font, 0, 70, {
3058
3062
  text: miiJson.meta.name,
3059
3063
  alignmentX: Jimp.HORIZONTAL_ALIGN_CENTER,
3060
3064
  alignmentY: Jimp.VERTICAL_ALIGN_MIDDLE
@@ -3077,61 +3081,64 @@ async function write3DSQR(miiJson, outPath, fflRes = getFFLRes()) {
3077
3081
  return imageBuffer;
3078
3082
  }
3079
3083
  function make3DSChild(dad,mom,options={}){
3080
- if(!["3ds","wii u"].includes(dad.console?.toLowerCase())){
3084
+ if(!["3ds","wii u"].includes(dad.meta.console?.toLowerCase())){
3081
3085
  dad=convertMii(dad,"wii");
3082
3086
  }
3083
- if(!["3ds","wii u"].includes(mom.console?.toLowerCase())){
3087
+ if(!["3ds","wii u"].includes(mom.meta.console?.toLowerCase())){
3084
3088
  mom=convertMii(dad,"wii");
3085
3089
  }
3086
- var g=options.gender||Math.floor(Math.random()*2)===1?"Male":"Female";
3090
+ var g=options.gender||Math.floor(Math.random()*2);
3087
3091
  var child={
3088
- "info":{
3092
+ "general":{
3089
3093
  "birthMonth":new Date().getMonth()+1,
3090
3094
  "birthday":new Date().getDay(),
3091
3095
  "height":64,
3092
3096
  "weight":64,
3093
- "creatorName":"",
3094
3097
  "gender":g,
3095
- "name":options.name||kidNames[g][Math.floor(Math.random()*kidNames[g].length)],
3096
3098
  "favColor":options.favColor||favCols[Math.floor(Math.random()*favCols.length)]
3097
3099
  },
3100
+ "meta":{
3101
+ "name":options.name||kidNames[g][Math.floor(Math.random()*kidNames[g].length)],
3102
+ "creatorName":"",
3103
+ },
3098
3104
  "perms":{
3099
3105
  "sharing":true,
3100
3106
  "copying":true
3101
3107
  },
3102
3108
  "hair":{
3103
- "style":[8,3],//Hardcoded
3104
- "col":Math.floor(Math.random()*2)===1?dad.hair.col:mom.hair.col,
3109
+ "page":8,//Hardcoded, needs to be generated
3110
+ "type":3,// ^
3111
+ "color":Math.floor(Math.random()*2)===1?dad.hair.color:mom.hair.color,
3105
3112
  "flipped":Math.floor(Math.random()*2)===0?true:false
3106
3113
  },
3107
3114
  "face":{
3108
3115
  "shape":Math.floor(Math.random()*2)===1?dad.face.shape:mom.face.shape,
3109
3116
  "feature":Math.floor(Math.random()*2)===1?dad.face.feature:mom.face.feature,
3110
- "makeup":g==="Male"?"None":Math.floor(Math.random()*2)===1?dad.face.makeup:mom.face.makeup
3117
+ "makeup":g===0?0:Math.floor(Math.random()*2)===1?dad.face.makeup:mom.face.makeup
3111
3118
  },
3112
3119
  "eyes":Math.floor(Math.random()*2)===1?dad.eyes:mom.eyes,
3113
3120
  "eyebrows":Math.floor(Math.random()*2)===1?dad.eyebrows:mom.eyebrows,
3114
3121
  "nose":Math.floor(Math.random()*2)===1?dad.nose:mom.nose,
3115
3122
  "mouth":Math.floor(Math.random()*2)===1?dad.mouth:mom.mouth,
3116
- "facialHair":g==="Female"?{
3117
- "mustacheType": 0,
3118
- "beardType": 0,
3119
- "col": "Black",
3120
- "mustacheSize": 4,
3121
- "mustacheYPos": 10
3122
- }:Math.floor(Math.random()*2)===0?dad.facialHair:mom.facialHair,
3123
+ "beard":{//Beards can never be generated for children allegedly, confirm before finishing
3124
+ "mustache":{
3125
+ "type":0,
3126
+ "mustacheSize": 4,
3127
+ "mustacheYPos": 10
3128
+ },
3129
+ "type": 0,
3130
+ "color": 0
3131
+ },
3123
3132
  "glasses":Math.floor(Math.random()*2)===1?dad.glasses:mom.glasses,
3124
- "mole":Math.floor(Math.random()*2)===1?dad.mole:mom.mole,
3125
- "creatorName":""
3133
+ "mole":Math.floor(Math.random()*2)===1?dad.mole:mom.mole
3126
3134
  };
3127
- child.eyebrows.col=child.hair.col;
3128
- var c=[skinCols.indexOf(mom.face.col),skinCols.indexOf(dad.face.col)];
3135
+ child.eyebrows.color=child.hair.color;
3136
+ var c=[mom.face.color,dad.face.color];
3129
3137
  if(c[0]>c[1]){
3130
3138
  c[1]=c[0];
3131
- c[0]=skinCols.indexOf(dad.face.col);
3139
+ c[0]=dad.face.color;
3132
3140
  }
3133
- child.face.col=skinCols[c[0]+Math.round((c[1]-c[0])/2)];
3134
- child.name=child.info.name;
3141
+ child.face.color=c[0]+Math.round((c[1]-c[0])/2);
3135
3142
  child.type="3DS";
3136
3143
  return child;
3137
3144
  }
@@ -3254,6 +3261,100 @@ function generateInstructions(mii,full){
3254
3261
  }
3255
3262
  }
3256
3263
 
3264
+ function miiHeightToFeetInches(value) {
3265
+ const minInches = 36; // 3'0"
3266
+ const midInches = 69; // 5'9"
3267
+ const maxInches = 84; // 7'0"
3268
+ const midPoint = 64;
3269
+
3270
+ let totalInches;
3271
+ if (value <= midPoint) {
3272
+ // Lower half: 0–64 maps to 36–69
3273
+ totalInches = minInches + (value / midPoint) * (midInches - minInches);
3274
+ }
3275
+ else {
3276
+ // Upper half: 64–127 maps to 69–84
3277
+ totalInches = midInches + ((value - midPoint) / (127 - midPoint)) * (maxInches - midInches);
3278
+ }
3279
+
3280
+ const feet = Math.floor(totalInches / 12);
3281
+ const inches = Math.round(totalInches % 12);
3282
+ return { feet, inches, totalInches };
3283
+ }
3284
+ function inchesToMiiHeight(totalInches) {
3285
+ const minInches = 36;
3286
+ const midInches = 69;
3287
+ const maxInches = 84;
3288
+ const midPoint = 64;
3289
+
3290
+ let value;
3291
+ if (totalInches <= midInches) {
3292
+ // Below or equal to midpoint
3293
+ value = ((totalInches - minInches) / (midInches - minInches)) * midPoint;
3294
+ } else {
3295
+ // Above midpoint
3296
+ value = midPoint + ((totalInches - midInches) / (maxInches - midInches)) * (127 - midPoint);
3297
+ }
3298
+
3299
+ return Math.round(Math.max(0, Math.min(value, 127)));
3300
+ }
3301
+ // ---- Tunable anchors (BMI breakpoints) ----
3302
+ const BMI_MIN = 16; // maps to Mii weight 0
3303
+ const BMI_MID = 23; // maps to Mii weight 64 (average look)
3304
+ const BMI_MAX = 40; // maps to Mii weight 127
3305
+
3306
+ // Convenience: clamp helper
3307
+ const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
3308
+
3309
+ /**
3310
+ * Convert real-world height & weight to Mii weight (0–127) using BMI.
3311
+ * @param {number} heightInches - Total height in inches
3312
+ * @param {number} weightLbs - Weight in pounds
3313
+ * @returns {number} - Mii weight index (0–127)
3314
+ */
3315
+ function heightWeightToMiiWeight(heightInches, weightLbs) {
3316
+ if (!heightInches || heightInches <= 0) throw new Error("heightInches must be > 0");
3317
+ const bmi = (703 * weightLbs) / (heightInches * heightInches);
3318
+
3319
+ let v;
3320
+ if (bmi <= BMI_MID) {
3321
+ // Map BMI_MIN..BMI_MID -> 0..64
3322
+ const t = (clamp(bmi, BMI_MIN, BMI_MID) - BMI_MIN) / (BMI_MID - BMI_MIN);
3323
+ v = 0 + t * 64;
3324
+ } else {
3325
+ // Map BMI_MID..BMI_MAX -> 64..127
3326
+ const t = (clamp(bmi, BMI_MID, BMI_MAX) - BMI_MID) / (BMI_MAX - BMI_MID);
3327
+ v = 64 + t * (127 - 64);
3328
+ }
3329
+ return Math.round(clamp(v, 0, 127));
3330
+ }
3331
+
3332
+ /**
3333
+ * Convert Mii weight (0–127) back to a realistic real-world weight (lbs)
3334
+ * for a given height, by inverting the BMI mapping above.
3335
+ * @param {number} heightInches - Total height in inches
3336
+ * @param {number} miiWeight - 0..127
3337
+ * @returns {{ pounds:number, bmi:number }}
3338
+ */
3339
+ function miiWeightToRealWeight(heightInches, miiWeight) {
3340
+ if (!heightInches || heightInches <= 0) heightInches=0;
3341
+ const v = clamp(miiWeight, 0, 127);
3342
+
3343
+ let bmi;
3344
+ if (v <= 64) {
3345
+ // Invert 0..64 -> BMI_MIN..BMI_MID
3346
+ const t = v / 64;
3347
+ bmi = BMI_MIN + t * (BMI_MID - BMI_MIN);
3348
+ } else {
3349
+ // Invert 64..127 -> BMI_MID..BMI_MAX
3350
+ const t = (v - 64) / (127 - 64);
3351
+ bmi = BMI_MID + t * (BMI_MAX - BMI_MID);
3352
+ }
3353
+
3354
+ const pounds = (bmi * heightInches * heightInches) / 703;
3355
+ return { pounds, bmi };
3356
+ }
3357
+
3257
3358
 
3258
3359
 
3259
3360
  module.exports = {
@@ -3275,5 +3376,10 @@ module.exports = {
3275
3376
 
3276
3377
  //make3DSChild, //WIP
3277
3378
 
3278
- generateInstructions
3379
+ generateInstructions,
3380
+
3381
+ miiHeightToFeetInches,
3382
+ inchesToMiiHeight,
3383
+ heightWeightToMiiWeight,
3384
+ miiWeightToRealWeight
3279
3385
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miijs",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Work with Mii characters in every possible way needed for your project.",
5
5
  "main": "index.js",
6
6
  "scripts": {