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 });