From 26bb4899c4cd0c6a9102e16c8c21d705011bd77b Mon Sep 17 00:00:00 2001 From: sensei <nobody@slash.goat> Date: Thu, 28 Dec 2017 18:04:00 -0700 Subject: [PATCH] family tree v0.6 --- src/js/familyTree.tw | 421 ++++++++++++++++++++++++++- src/pregmod/managePersonalAffairs.tw | 15 +- src/uncategorized/slaveInteract.tw | 15 +- 3 files changed, 435 insertions(+), 16 deletions(-) diff --git a/src/js/familyTree.tw b/src/js/familyTree.tw index 067edda6ed0..58eda855acf 100644 --- a/src/js/familyTree.tw +++ b/src/js/familyTree.tw @@ -1,12 +1,12 @@ :: FamilyTreeJS [script] +'use strict'; var lastActiveSlave, lastSlaves, lastPC; /* To use, add something like: -<div id="editFamily"> - <div id="graph"></div> +<div id='familyTree'> </div> <<run updateFamilyTree($activeSlave, $slaves, $PC)>> @@ -20,6 +20,423 @@ If there's no active slave, you can do: */ +var d3scr = document.createElement('script'); +d3scr.setAttribute('type', 'text/javascript'); +d3scr.setAttribute('src', 'https://d3js.org/d3.v4.min.js'); +document.getElementsByTagName('head')[0].appendChild(d3scr); + +window.renderFamilyTree = function(slaves, filterID) { + + var ftreeWidth,ftreeHeight; + var chartWidth, chartHeight; + var margin; + var svg = d3.select('#familyTree').append('svg'); + var chartLayer = svg.append('g').classed('chartLayer', true); + + var range = 100; + var data = buildFamilyTree(slaves, filterID); + + initFtreeSVG(data); + runFtreeSim(data); + + function initFtreeSVG(data) { + ftreeWidth = data.nodes.length * 45; + ftreeHeight = data.nodes.length * 35; + if(ftreeWidth < 600) { + ftreeWidth = 600; + } + if(ftreeHeight < 480) { + ftreeHeight = 480; + } + + margin = {top:0, left:0, bottom:0, right:0 } + + + chartWidth = ftreeWidth - (margin.left+margin.right) + chartHeight = ftreeHeight - (margin.top+margin.bottom) + + svg.attr('width', ftreeWidth).attr('height', ftreeHeight) + + var defs = svg.append('defs'); + + svg.append('defs').append('marker') + .attr('id','arrowhead') + .attr('viewBox','-0 -5 10 10') + .attr('refX',13) + .attr('refY',0) + .attr('orient','auto') + .attr('markerWidth',13) + .attr('markerHeight',13) + .attr('xoverflow','visible') + .append('svg:path') + .attr('d', 'M 0,-1 L 5,0 L 0,1') + .attr('fill', '#a1a1a1') + .style('stroke','none'); + + chartLayer + .attr('width', chartWidth) + .attr('height', chartHeight) + .attr('transform', 'translate('+[margin.left, margin.top]+')') + } + + function runFtreeSim(data) { + var simulation = d3.forceSimulation() + .force('link', d3.forceLink().id(function(d) { return d.index })) + .force('collide',d3.forceCollide( function(d){ return 60; }).iterations(4) ) + .force('charge', d3.forceManyBody().strength(-200).distanceMin(100).distanceMax(1000)) + .force('center', d3.forceCenter(chartWidth / 2, chartHeight / 2)) + .force('y', d3.forceY(100)) + .force('x', d3.forceX(200)) + + var link = svg.append('g') + .attr('class', 'link') + .selectAll('link') + .data(data.links) + .enter() + .append('line') + .attr('marker-end','url(#arrowhead)') + .attr('stroke', function(d) { + if(d.type == 'homologous') { + return '#862d59'; + } else if(d.type == 'paternal') { + return '#24478f'; + } else { + return '#aa909b'; + } + }) + .attr('stroke-width', 2) + .attr('fill', 'none'); + + var node = svg.selectAll('.node') + .data(data.nodes) + .enter().append('g') + .attr('class', 'node') + .call(d3.drag() + .on('start', dragstarted) + .on('drag', dragged) + .on('end', dragended)); + + node.append('circle') + .attr('r', function(d){ return d.r }) + .attr('stroke', '#5a5a5a') + .attr('class', 'node-circle') + .attr('r', 20); + + node.append('text') + .text(function(d) { + var ssym; + if(d.ID == -1) { + ssym = ''; + } else if(d.dick > 0 && d.vagina > -1) { + ssym = '☿' + } else if (d.dick > 0) { + ssym = '♂'; + } else if (d.vagina > -1) { + ssym = '♀'; + } else { + ssym = '?'; + } + return d.name + '('+ssym+')'; + }) + .attr('dy', -5) + .attr('dx', function(d) { return -(6*d.name.length)/2; }) + .attr('class', 'node-text') + .style('fill', function(d) { + if(d.is_mother && d.is_father) { + return '#b84dff'; + } else if(d.is_father) { + return '#00ffff'; + } else if(d.is_mother) { + return '#ff3399'; + } else if(d.unborn) { + return '#a3a3c2'; + } else { + return '#66cc66'; + } + }); + + var circles = svg.selectAll('.node-circle'); + var texts = svg.selectAll('.node-text'); + + var ticked = function() { + link + .attr('x1', function(d) { return d.source.x; }) + .attr('y1', function(d) { return d.source.y; }) + .attr('x2', function(d) { return d.target.x; }) + .attr('y2', function(d) { return d.target.y; }); + + node + .attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";}); + } + + simulation.nodes(data.nodes) + .on('tick', ticked); + + simulation.force('link') + .links(data.links); + + function dragstarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(d) { + d.fx = d3.event.x; + d.fy = d3.event.y; + } + + function dragended(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + } +}; + +window.buildFamilyTree = function(slaves = SugarCube.State.variables.slaves, filterID) { + var family_graph = { + nodes: [], + links: [] + }; + var node_lookup = {}; + var preset_lookup = { + '-2': 'Social elite', + '-3': 'Client', + '-4': 'Former master', + '-5': 'An arcology owner', + '-6': 'A citizen' + }; + var outdads = {}; + var outmoms = {}; + var kids = {}; + + var fake_pc = { + slaveName: SugarCube.State.variables.PC.name + '(You)', + mother: SugarCube.State.variables.PC.mother, + father: SugarCube.State.variables.PC.father, + dick: SugarCube.State.variables.PC.dick, + vagina: SugarCube.State.variables.PC.vagina, + ID: SugarCube.State.variables.PC.ID + }; + var charList = [fake_pc]; + charList.push.apply(charList, slaves); + charList.push.apply(charList, SugarCube.State.variables.tanks); + + var unborn = {}; + for(var i = 0; i < SugarCube.State.variables.tanks.length; i++) { + unborn[SugarCube.State.variables.tanks[i].ID] = true; + } + + for(var i = 0; i < charList.length; i++) { + var mom = charList[i].mother; + var dad = charList[i].father; + + if(mom) { + if(!kids[mom]) { + kids[mom] = {}; + } + kids[mom].mother = true; + } + if(dad) { + if(!kids[dad]) { + kids[dad] = {}; + } + kids[dad].father = true; + } + } + + for(var i = 0; i < charList.length; i++) { + var character = charList[i]; + if(character.mother == 0 && character.father == 0 && !kids[character.ID]) { + continue; + } + var mom = character.mother; + if(mom < -6) { + if(typeof outmoms[mom] == 'undefined') { + outmoms[mom] = []; + } + outmoms[mom].push(character.slaveName); + } else if(mom < 0 && typeof node_lookup[mom] == 'undefined' && typeof preset_lookup[mom] != 'undefined') { + node_lookup[mom] = family_graph.nodes.length; + charList.push({ID: mom, mother: 0, father: 0, is_father: true, dick: 0, vagina: 1, slaveName: preset_lookup[mom]}); + } + + var dad = character.father; + if(dad < -6) { + if(typeof outdads[dad] == 'undefined') { + outdads[dad] = []; + } + outdads[dad].push(character.slaveName); + } else if(dad < 0 && typeof node_lookup[dad] == 'undefined' && typeof preset_lookup[dad] != 'undefined') { + node_lookup[dad] = family_graph.nodes.length; + charList.push({ID: dad, mother: 0, father: 0, is_father: true, dick: 1, vagina: -1, slaveName: preset_lookup[dad]}); + } + } + var mkeys = Object.keys(outmoms); + for(var i = 0; i < mkeys.length; i++) { + var name; + var key = mkeys[i]; + var names = outmoms[key]; + if(names.length == 1) { + name = names[0]; + } else if(names.length == 2) { + name = names.join(' and '); + } else { + names[-1] = 'and '+names[-1]; + name = names.join(', '); + } + node_lookup[key] = family_graph.nodes.length; + //Outside extant slaves set + charList.push({ID: key, mother: 0, father: 0, is_mother: true, dick: 0, vagina: 1, slaveName: name+"'s mother"}); + } + + var dkeys = Object.keys(outdads); + for(var i = 0; i < dkeys.length; i++) { + var name; + var key = dkeys[i]; + var names = outdads[key]; + if(names.length == 1) { + name = names[0]; + } else if(names.length == 2) { + name = names.join(' and '); + } else { + names[-1] = 'and '+names[-1]; + name = names.join(', '); + } + node_lookup[key] = family_graph.nodes.length; + //Outside extant slaves set + charList.push({ID: key, mother: 0, father: 0, is_father: true, dick: 1, vagina: -1, slaveName: name+"'s father"}); + } + + var charHash = {}; + for(var i = 0; i < charList.length; i++) { + charHash[charList[i].ID] = charList[i]; + } + + var related = {}; + var seen = {}; + var saveTree = {}; + function relatedTo(character, targetID, relIDs = {tree: {}, related: false}) { + relIDs.tree[character.ID] = true; + if(related[character.ID]) { + relIDs.related = true; + return relIDs; + } + if(character.ID == targetID) { + relIDs.related = true; + } + if(seen[character.ID]) { + return relIDs; + } + seen[character.ID] = true; + if(character.mother != 0) { + if(charHash[character.mother]) { + relatedTo(charHash[character.mother], targetID, relIDs); + } + } + if(character.father != 0) { + if(charHash[character.father]) { + relatedTo(charHash[character.father], targetID, relIDs); + } + } + return relIDs; + } + if(filterID) { + if(charHash[filterID]) { + var relIDs = relatedTo(charHash[filterID], filterID); + for(var k in relIDs.tree) { + related[k] = true; + } + for(var i = 0; i < charList.length; i++) { + if(charHash[charList[i].ID]) { + var pRelIDs = relatedTo(charHash[charList[i].ID], filterID); + if(pRelIDs.related) { + for(var k in pRelIDs.tree) { + related[k] = true; + if(saveTree[k]) { + for(var k2 in saveTree[k].tree) { + related[k2] = true; + } + } + } + } + saveTree[charList[i].ID] = pRelIDs; + } + } + } + } + + for(var i = 0; i < charList.length; i++) { + var character = charList[i]; + var char_id = character.ID; + if(character.mother == 0 && character.father == 0 && !kids[char_id]) { + continue; + } + if(filterID && !related[char_id]) { + continue; + } + node_lookup[char_id] = family_graph.nodes.length; + var char_obj = { + id: char_id, + name: character.slaveName, + dick: character.dick, + unborn: !!unborn[char_id], + vagina: character.vagina + }; + if(kids[char_id]) { + char_obj.is_mother = !!kids[char_id].mother; + char_obj.is_father = !!kids[char_id].father; + } else { + char_obj.is_mother = false; + char_obj.is_father = false; + } + family_graph.nodes.push(char_obj); + } + + for(var i = 0; i < charList.length; i++) { + var character = charList[i]; + var char_id = character.ID; + if(character.mother == 0 && character.father == 0 && !kids[char_id]) { + continue; + } + if(filterID && !related[char_id]) { + if(related[character.mother]) { + console.log('wtf, mom'); + } + if(related[character.father]) { + console.log('wtf, dad'); + } + continue; + } + if(typeof node_lookup[character.mother] != 'undefined') { + var ltype; + if(character.mother == character.father) { + ltype = 'homologous'; + } else { + ltype = 'maternal'; + } + family_graph.links.push({ + type: ltype, + target: node_lookup[char_id]*1, + source: node_lookup[character.mother]*1 + }); + } + if(character.mother == character.father) { + continue; + } + if(typeof node_lookup[character.father] != 'undefined') { + family_graph.links.push({ + type: 'paternal', + target: node_lookup[char_id]*1, + source: node_lookup[character.father]*1 + }); + } + } + return family_graph; +}; + window.updateFamilyTree = function(activeSlave = lastActiveSlave, slaves = lastSlaves, PC = lastPC) { lastActiveSlave = activeSlave; lastSlaves = slaves; diff --git a/src/pregmod/managePersonalAffairs.tw b/src/pregmod/managePersonalAffairs.tw index c85fb063f63..eb59d31eaab 100644 --- a/src/pregmod/managePersonalAffairs.tw +++ b/src/pregmod/managePersonalAffairs.tw @@ -428,13 +428,14 @@ __Rumors__ <<if $familyTesting == 1>> <br><br> <span id="family"> - <<link "Pull up the file on your family tree.">> - <<replace #family>> - <div id="editFamily"><div id="graph"></div></div> - <<run updateFamilyTree(null, $slaves, $PC)>> - <script>updateFamilyTree()</script> - <</replace>> - <</link>> + <div id="familyTree"></div> + <span id="familyTreeLink"> + <<link "Pull up the file on your family tree.">> + <<replace #familyTreeLink>> + <<run renderFamilyTree($slaves, -1)>> + <</replace>> + <</link>> + </span> </span> <<if totalPlayerRelatives($PC) > 0>> <<PlayerFamily>> diff --git a/src/uncategorized/slaveInteract.tw b/src/uncategorized/slaveInteract.tw index 7fc2185cc7f..2d364bce57e 100644 --- a/src/uncategorized/slaveInteract.tw +++ b/src/uncategorized/slaveInteract.tw @@ -342,13 +342,14 @@ <<if $familyTesting == 1>> <br><br> <span id="family"> - <<link "Pull up the file on her family tree.">> - <<replace #family>> - <div id="editFamily"><div id="graph"></div></div> - <<run updateFamilyTree($activeSlave, $slaves, $PC)>> - <script>updateFamilyTree()</script> - <</replace>> - <</link>> + <div id="familyTree"></div> + <span id="familyTreeLink"> + <<link "Pull up the file on her family tree.">> + <<replace #familyTreeLink>> + <<run renderFamilyTree($slaves, $activeSlave.ID)>> + <</replace>> + <</link>> + </span> </span> <</if>> -- GitLab