1 /// 2 module isodi.model; 3 4 import core.time; 5 import std.string; 6 import std.random; 7 8 import isodi.pack; 9 import isodi.tests; 10 import isodi.object3d; 11 import isodi.resource; 12 13 14 @safe: 15 16 17 /// Represents a 3D model. 18 abstract class Model : Object3D, WithDrawableResources { 19 20 /// Position in the model is relative to the model's bottom, so if a cell is placed at the same position 21 /// as the model, the model will be standing on the cell. 22 mixin Object3D.Implement; 23 24 private { 25 26 static size_t nextID; 27 size_t _id; 28 29 } 30 31 /// Type of the model. 32 const string type; 33 34 /// Active animations. 35 protected Animation[] animations; 36 37 /// Seed to use for RNG calls related to generation of this model's resources. 38 /// 39 /// It's preferred to sum this with a magic number to ensure unique combinations. 40 const ulong seed; 41 42 /// Create a new model. 43 /// Params: 44 /// display = Display to create the model for. 45 /// type = Skeleton to use for the model. 46 this(Display display, const string type) { 47 48 // TODO: ModelBuilder for Isodi editors. 49 50 super(display); 51 this._id = nextID++; 52 this.type = type; 53 this.seed = unpredictableSeed; 54 55 } 56 57 @property 58 size_t id() const { return _id; } 59 60 /// Ditto 61 static Model make(Display display, const string type) { 62 63 return Renderer.createModel(display, type); 64 65 } 66 67 /// Change the variant used for the node with given ID. 68 /// Params: 69 /// id = ID of the node to change. 70 /// variant = Variant of the bone to be set. 71 abstract void changeVariant(string id, string variant); 72 73 /// Randomize the variant used for the node with given ID. 74 /// Params: 75 /// id = ID of the node to change. 76 void changeVariant(string id) { 77 78 assert(0, "unimplemented"); 79 80 } 81 82 /// Run an animation. 83 /// Params: 84 /// type = Type of the animation. 85 /// duration = Time it should take for one loop of the animation to complete. 86 /// times = How many times the animation should be ran. 87 void animate(string type, Duration duration, uint times = 1) { 88 89 // Get the resource 90 uint frameCount; // @suppress(dscanner.suspicious.unmodified) 91 auto resource = display.packs.getAnimation(type, frameCount); 92 93 // Push the animation 94 animations ~= Animation( 95 cast(float) frameCount / duration.total!"msecs" * 1000f, 96 times, 97 resource.match 98 ); 99 100 } 101 102 /// Run an animation indefinitely. 103 /// Params: 104 /// type = Type of the animation 105 /// duration = Time it should take for one loop of the animation to complete. 106 void animateInf(string type, Duration duration) { 107 108 animate(type, duration, 0); 109 110 } 111 112 // TODO: stopAnimation 113 114 /// 115 protected Pack.Resource!string getBone(const SkeletonNode node) @trusted { 116 117 // TODO: add support for node.variants 118 119 auto rng = Mt19937_64(seed + node.parent); 120 121 // Get the texture 122 auto glob = display.packs.packGlob(node.name.format!"models/bone/%s/*.png"); 123 const file = glob.matches.choice(rng); 124 125 return Pack.Resource!string( 126 file, 127 glob.pack.getOptions(file) 128 ); 129 130 } 131 132 } 133 134 mixin DisplayTest!((display) { 135 136 // Model 1 137 display.addCell(Position(), "grass"); 138 with (display.addModel(Position(), "wraith-white")) { 139 140 animateInf("breath", 6.seconds); 141 animate("crab", 1.seconds); 142 143 } 144 145 // Model 2 146 display.addCell(position(2, 0), "grass"); 147 display.addModel(position(2, 0), "wraith-white"); 148 149 // Add a cell behind 150 display.addCell(position(2, 1, Height(4, 5)), "grass"); 151 152 });