Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable/Disable Player Aging #18

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions .idea/codeStyles/codeStyleConfig.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/facesjs.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

121 changes: 107 additions & 14 deletions public/editor.html
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,80 @@ <h1 class="mb-4 mt-2">
</div>
</div>
</div>
<br />
<div class="p-3 bg-warning bg-gradient">
<div class="row">
<div class="col-10"><h4>Aging</h4></div>
<div class="col-2 text-right">
<input
class="form-check-input random-group randomize-smile-lines"
type="checkbox"
value=""
checked
id="randomize-aging"
/>
</div>
</div>
<div class="row">
<div class="col-10">
<div class="form-group">
<select class="form-control" id="aging-enabled">
</select>
</div>
</div>
<div class="col-2 text-right">
&nbsp;
</div>
</div>
<div class="row">
<div class="col-5">
<label for="aging-age">Age</label>
</div>
<div class="col-5 form-group">
<input
type="number"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change this to type="range" and you get a nifty slider. Should display the age as text next to it too.

class="form-control"
id="aging-age"
min="18"
max="40"
step="1"
/>
</div>
<div class="col-2 text-right">
<input
class="form-check-input random-attribute randomize-aging"
type="checkbox"
value=""
checked
id="randomize-aging-age"
/>
</div>
</div>
<div class="row">
<div class="col-5">
<label for="aging-maturity">Maturity</label>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like having a "maturity" variable but I think it needs to be renamed, at least in the UI, to help people understand what it does. Maybe rename to "Aging speed" or something?

</div>
<div class="col-5 form-group">
<input
type="number"
class="form-control"
id="aging-maturity"
min="-3"
max="3"
step="1"
/>
</div>
<div class="col-2 text-right">
<input
class="form-check-input random-attribute randomize-aging"
type="checkbox"
value=""
checked
id="randomize-aging-maturity"
/>
</div>
</div>
</div>
</div>
<div class="col">
<div class="p-3 bg-info bg-gradient">
Expand Down Expand Up @@ -353,7 +427,7 @@ <h1 class="mb-4 mt-2">
</div>
<div class="col-2 text-right">
<input
class="form-check-input random-attribute randomize-hair"
class="form-check-input random-attribute randomize-long-hair"
type="checkbox"
value=""
checked
Expand Down Expand Up @@ -890,27 +964,27 @@ <h1 class="mb-4 mt-2">

let face;
if (location.hash.length <= 1) {
face = faces.generate();
face = faces.generate({ aging: { enabled: true } });
} else {
try {
face = JSON.parse(atob(location.hash.slice(1)));
} catch (error) {
console.error(error);
face = faces.generate();
face = faces.generate({ aging: { enabled: true } });
}
}

const randomizeFace = (oldFace, newFace) => {
Array.from(document.getElementsByClassName("random-attribute")).forEach(
(elem) => {
if (!elem.checked) {
const attr = elem.id.split("-").slice(1);
if (attr.length === 1) newFace[attr[0]] = oldFace[attr[0]];
else if (!isNaN(parseInt(attr[1]))) {
const idx = parseInt(attr[1]);
newFace[attr[0]][idx] = oldFace[attr[0]][idx];
} else if (attr.length === 2)
newFace[attr[0]][attr[1]] = oldFace[attr[0]][attr[1]];
const parts = elem.id.split("-").slice(1);
if (parts.length === 1) newFace[parts[0]] = oldFace[parts[0]];
else if (!isNaN(parseInt(parts[1]))) {
const idx = parseInt(parts[1]);
newFace[parts[0]][idx] = oldFace[parts[0]][idx];
} else if (parts.length === 2)
newFace[parts[0]][parts[1]] = oldFace[parts[0]][parts[1]];
}
}
);
Expand All @@ -929,16 +1003,27 @@ <h1 class="mb-4 mt-2">
};

const initializeSelectOptions = () => {
let selectElement, optionElement;
for (const feature of Object.keys(faces.svgsIndex)) {
const options = faces.svgsIndex[feature];
const selectElement = document.getElementById(`${feature}-id`);
selectElement = document.getElementById(`${feature}-id`);
for (const option of options) {
const optionElement = document.createElement("option");
optionElement = document.createElement("option");
optionElement.value = option;
optionElement.text = option;
selectElement.add(optionElement, null);
}
}
selectElement = document.getElementById("aging-enabled");
optionElement = document.createElement("option");
optionElement.selected = true;
optionElement.value = "true";
optionElement.text = "Enabled";
selectElement.add(optionElement, null);
optionElement = document.createElement("option");
optionElement.value = "false";
optionElement.text = "Disabled";
selectElement.add(optionElement, null);
};

const isValue = (obj) =>
Expand Down Expand Up @@ -975,6 +1060,12 @@ <h1 class="mb-4 mt-2">
if (typeof oldValue === "number") {
return parseFloat(event.target.value);
}
if (
typeof oldValue === "boolean" &&
typeof event.target.value !== "boolean"
) {
return event.target.value === "true";
}
if (typeof oldValue === "boolean") {
return event.target.checked;
}
Expand Down Expand Up @@ -1008,8 +1099,10 @@ <h1 class="mb-4 mt-2">
}

document.getElementById("randomize").addEventListener("click", () => {
const oldFace = face;
face = randomizeFace(oldFace, faces.generate());
face = randomizeFace(
face,
faces.generate({ aging: { enabled: face.aging.enabled } })
);
initializeFormValues();
updateDisplay();
});
Expand Down
124 changes: 117 additions & 7 deletions src/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,93 @@ type FeatureInfo = {
scaleFatness?: true;
};

const hashCode = (str) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const character = str.charCodeAt(i);
hash = (hash << 5) - hash + character;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash);
};

const deterministicRandom = (face: Face) => {
const hash =
hashCode(face.body.id) +
hashCode(face.body.color) +
hashCode(face.head.id) +
hashCode("" + face.fatness) +
hashCode(face.hair.id) +
hashCode(face.hair.color);
return (hash % 1000) / 1000;
};

const ageHair = (hairId: String) => {
switch (hairId) {
case "afro":
return "short";
case "afro2":
return "short";
case "blowoutFade":
return "cropFade2";
case "cornrows":
return "short-fade";
case "curly3":
return "short3";
case "dreads":
return "short-fade";
case "emo":
return "short2";
case "faux-hawk":
return "short3";
case "fauxhawk-fade":
return "short-fade";
case "high":
return "short";
case "juice":
return "short2";
case "longHair":
return "short-fade";
case "shaggy2":
return "shaggy1";
case "short-bald":
return "short-bald";
case "shortBangs":
return "short-bald";
case "spike":
return "short";
case "spike2":
return "short";
case "spike3":
return "short";
case "spike4":
return "short";
case "tall-fade":
return "crop-fade";
default:
return "short-fade";
}
};

const drawFeature = (svg: SVGSVGElement, face: Face, info: FeatureInfo) => {
const feature = face[info.name];
const feature = Object.assign({}, face[info.name]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change? I think it doesn't do anything, but I may be missing something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change creates a copy of face[info.name] instead of passing the reference, this way face doesn't get changed even if the actual features drawn vary. The reason why I'm doing this will make sense after reading below.

My thinking and overall strategy for implementing the aging feature was that the parts and their values would act as master DNA for the person if aging.enabled = true. Another way to think of this is you are supplying what features the person can and will ultimately get as he ages. So for example, if the generated face has forehead lines, those lines won't be drawn until the person is in his older 30s. This way, the same JSON face object can go through the aging progression simply by incrementing the age value, rather than having to keep track of adding and subtracting parts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with that is you'd then have to generate every new face with a bunch of wrinkles and other old people features. Otherwise you'd have some that never age.

Ultimately you can get the same result either way, I just think it'd be more intuitive to have the base face object be the young version, and then add on stuff to represent aging. Because that's what actually happens as real humans age :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm more considering generating new faces rather than aging existing faces. I figured new face objects would have to be generated anyway to include the aging key/values.

However, I think by making the base face object the young version, it won't allow users to specify what aging lines will eventually occur when the face ages. Or if the deterministic aging isn't varied enough, you might see too many common combinations of heads/lines. I'd rather have the option to choose what my face will look like at 35 and then have the younger versions of the face be determined, especially since its basically just removal of lines. I do realize that's now how it works in real life but I believe this way allows for full customization, i.e. what if I want a different aging line for a face then what the deterministic random would apply.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way of phrasing that is... you pick all the facial features you want for your guy. Some will appear/disappear based on age. If you say he has a bunch of wrinkles, he won't when he's young. If you say he has long hair, he might not when he's old. I think that makes enough sense for people to understand.

if (!feature || !svgs[info.name]) {
return;
}
if (face.aging && face.aging.enabled) {
if (
info.name === "hair" &&
face.aging.age + face.aging.maturity / 2 >= 30 &&
deterministicRandom(face) < 0.5
)
feature.id = ageHair(feature.id);
else if (
info.name === "hairBg" &&
face.aging.age + face.aging.maturity / 2 >= 27 &&
deterministicRandom(face) < 0.75
)
feature.id = "none";
}

// @ts-ignore
let featureSVGString = svgs[info.name][feature.id];
Expand All @@ -111,14 +193,15 @@ const drawFeature = (svg: SVGSVGElement, face: Face, info: FeatureInfo) => {

// @ts-ignore
if (feature.shave) {
let shave;
if (face.aging && face.aging.enabled)
if (face.aging.age + face.aging.maturity > 23) shave = feature.shave;
else shave = "rgba(0,0,0,0)";
else shave = feature.shave;
// @ts-ignore
featureSVGString = featureSVGString.replace("$[faceShave]", feature.shave);
}

// @ts-ignore
if (feature.shave) {
featureSVGString = featureSVGString.replace("$[faceShave]", shave);
// @ts-ignore
featureSVGString = featureSVGString.replace("$[headShave]", feature.shave);
featureSVGString = featureSVGString.replace("$[headShave]", shave);
}

featureSVGString = featureSVGString.replace("$[skinColor]", face.body.color);
Expand Down Expand Up @@ -311,6 +394,33 @@ const display = (
];

for (const info of featureInfos) {
if (face.aging && face.aging.enabled) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to what I wrote about hair... maybe switch this around, so some facial lines are just never (or more rarely) included in the base face object, and then apply them here (maybe again with deterministic randomness). I think that would make it easier to have aging appear in all faces. Since currently, when I'm playing around with this, a lot of faces are not impacted by aging.

Copy link
Contributor Author

@gtabot gtabot Mar 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responding to the last part of your comment. My understanding is that you implemented this version in BBGM and you didn't see many aged faces? If so, first, were you able to inject the age value in aging.age?

If so, my thoughts are the way aging is currently set up (to use the parts as master DNA) a lot of your currently generated face models would not show the effects of aging. This is because I assume the proportion of faces that have aging lines are low combined with the fact that of these faces, only some are actually old enough to show effects of aging. Using this version of faces.js, most if not all generated faces should be randomly assigned aging lines (unless they're blessed with great genes) and those lines would appear appropriately as they age.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I very arbitrarily chose different ages for different lines to appear. They could easily be too high. There could also be a variable called maturity that can span +/- 3 or something which can add an element of variation across the players on when they reach these arbitrary age thresholds.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that you implemented this version in BBGM and you didn't see many aged faces?

No, just playing around in the editor UI. But it's the same thing, BBGM generates random faces the same way the editor does.

Also, I very arbitrarily chose different ages for different lines to appear. They could easily be too high. There could also be a variable called maturity that can span +/- 3 or something which can add an element of variation across the players on when they reach these arbitrary age thresholds.

Probably a good idea. On second thought, what I wrote before about using deterministic random numbers to determine aging might be kind of annoying, because then users wouldn't be able to control it...

lol complicated stuff the more you think about it!

if (
info.name === "miscLine" &&
face.aging.age + face.aging.maturity >= 22 &&
face.miscLine.id.startsWith("freckles")
)
continue;
if (
info.name === "miscLine" &&
face.aging.age + face.aging.maturity < 25 &&
face.miscLine.id.startsWith("chin")
)
continue;
if (
info.name === "smileLine" &&
face.aging.age + face.aging.maturity < 27
)
continue;
if (info.name === "eyeLine" && face.aging.age + face.aging.maturity < 30)
continue;
if (
info.name === "miscLine" &&
face.aging.age + face.aging.maturity < 34 &&
face.miscLine.id.startsWith("forehead")
)
continue;
}
drawFeature(svg, face, info);
}
};
Expand Down
Loading